2008-11-17 19 views
27

Existe la necesidad de que un cliente registre cada cambio de datos en una tabla de registro con el usuario real que realizó la modificación. La aplicación utiliza un usuario de SQL para acceder a la base de datos, pero debemos registrar la identificación de usuario "real".Registrando cada cambio de datos con Entity Framework

Podemos hacerlo en t-sql escribiendo disparadores para cada inserción y actualización de tabla, y usando context_info para almacenar la identificación de usuario. Pasamos el ID de usuario a un procedimiento almacenado, almacenamos el ID de usuario en el contexto, y el desencadenador podría usar esta información para escribir filas de registro en la tabla de registro.

No encuentro el lugar o la forma en la que puedo hacer algo similar con EF. Entonces, el objetivo principal es: si realizo un cambio en los datos a través de EF, me gustaría registrar el cambio de datos exacto en una tabla de forma semiautomática (por lo tanto, no deseo verificar si hay cambios en todos los campos). guardando el objeto). Estamos usando EntitySQL.

Desafortunadamente tenemos que seguir con SQL 2000, por lo que la captura de cambios de datos introducida en SQL2008 no es una opción (pero quizás tampoco sea la correcta para nosotros).

¿Alguna idea, enlace o punto de partida?

[Editar] Algunas notas: mediante el uso de ObjectContext.SavingChanges eventhandler, puedo conseguir el punto donde pueda inyectar la instrucción SQL para inicializar el contextinfo. Sin embargo, no puedo mezclar el EF y el SQL estándar. De modo que puedo obtener EntityConnection pero no puedo ejecutar una instrucción T-SQL usándolo. O puedo obtener la cadena de conexión de EntityConnection y crear una SqlConnection basada en ella, pero será una conexión diferente, por lo que el contexto no afectará el guardado realizado por el EF.

He intentado lo siguiente en el controlador SavingChanges:

testEntities te = (testEntities)sender; 
DbConnection dc = te.Connection; 
DbCommand dcc = dc.CreateCommand(); 
dcc.CommandType = CommandType.StoredProcedure; 
DbParameter dp = new EntityParameter(); 
dp.ParameterName = "userid"; 
dp.Value = textBox1.Text; 
dcc.CommandText = "userinit"; 
dcc.Parameters.Add(dp); 
dcc.ExecuteNonQuery(); 

de error: El valor de EntityCommand.CommandText no es válida para un comando StoredProcedure. Lo mismo con SqlParameter en lugar de EntityParameter: SqlParameter no se puede utilizar.

StringBuilder cStr = new StringBuilder("declare @tx char(50); set @tx='"); 
cStr.Append(textBox1.Text); 
cStr.Append("'; declare @m binary(128); set @m = cast(@tx as binary(128)); set context_info @m;"); 

testEntities te = (testEntities)sender; 
DbConnection dc = te.Connection; 
DbCommand dcc = dc.CreateCommand(); 
dcc.CommandType = CommandType.Text; 
dcc.CommandText = cStr.ToString(); 
dcc.ExecuteNonQuery(); 

Error: La sintaxis de la consulta no es válida.

Así que aquí estoy, atascado para crear un puente entre Entity Framework y ADO.NET. Si puedo hacerlo funcionar, publicaré una prueba de concepto.

+4

serio el usuario quiere que uses EF y luego requiere que se pega con SQL 2000? –

+1

Hay una publicación interesante relacionada con la auditoría [aquí] (http://blogs.msdn.com/b/simonince/archive/2009/04/20/auditing-data-changes-in-the-entity-framework-part- 2.aspx), ¿qué piensas? – ibiza

+0

Seis años después y sigo pensando que este es un gran enfoque. Los desencadenadores son mucho mejores para la auditoría que el código de nivel de aplicación, y esto proporciona una forma ingeniosa de obtener información de usuario 'real' en la capa de db sin contaminar el código de la aplicación. – Rory

Respuesta

12

¿Qué le parece manejar el contexto. SavingChanges?

+0

Sí, eso es lo que me gustaría evitar. :-) Sería bueno manejar todo esto de forma automática. Ya tenemos el generador de disparadores para manejar la parte de registro. El eslabón perdido es que no podemos pasar la identificación del usuario al disparador. – Biri

+0

No puede usar SavingChanges para establecer una ID de usuario en la información de contexto? http://msdn.microsoft.com/en-us/library/ms187768.aspx –

+0

Oh, mierda. La solución más fácil y estábamos pensando en algo muy sofisticado. Gracias por abrirme los ojos. – Biri

3

¿Ha intentado agregar el procedimiento almacenado al modelo de su entidad?

+0

Sí, Lo hice. Craig señaló en la dirección correcta, s o Publicaré un POC al lado. – Biri

9

Finalmente, con la ayuda de Craig, aquí hay una prueba de concepto. Necesita más pruebas, pero para el primer vistazo está funcionando.

Primero: Creé dos tablas, una para los datos uno para el registro.

-- This is for the data 
create table datastuff (
    id int not null identity(1, 1), 
    userid nvarchar(64) not null default(''), 
    primary key(id) 
) 
go 

-- This is for the log 
create table naplo (
    id int not null identity(1, 1), 
    userid nvarchar(64) not null default(''), 
    datum datetime not null default('2099-12-31'), 
    primary key(id) 
) 
go 

Segundo: cree un desencadenador para insertar.

create trigger myTrigger on datastuff for insert as 

    declare @User_id int, 
     @User_context varbinary(128), 
     @User_id_temp varchar(64) 

    select @User_context = context_info 
     from master.dbo.sysprocesses 
     where [email protected]@spid 

    set @User_id_temp = cast(@User_context as varchar(64)) 

    declare @insuserid nvarchar(64) 

    select @insuserid=userid from inserted 

    insert into naplo(userid, datum) 
     values(@User_id_temp, getdate()) 

go 

También debe crear un disparador de actualización, que será un poco más sofisticado, ya que tiene que comprobar todos los campos de contenido cambiado.

La tabla de registro y el activador deben extenderse para almacenar la tabla y el campo que se crea/modifica, pero espero que haya tenido la idea.

Tercero: cree un procedimiento almacenado que complete el ID de usuario a la información de contexto de SQL.

create procedure userinit(@userid varchar(64)) 
as 
begin 
    declare @m binary(128) 
    set @m = cast(@userid as binary(128)) 
    set context_info @m 
end 
go 

Estamos listos con el lado SQL. Aquí viene la parte C#.

Cree un proyecto y agregue un EDM al proyecto. El EDM debe contener la tabla de datos (o las tablas que necesita ver para ver los cambios) y el SP.

Ahora haga algo con el objeto entidad (por ejemplo, agregue un nuevo objeto de datos) y enganche al evento SavingChanges.

using (testEntities te = new testEntities()) 
{ 
    // Hook to the event 
    te.SavingChanges += new EventHandler(te_SavingChanges); 

    // This is important, because the context info is set inside a connection 
    te.Connection.Open(); 

    // Add a new datastuff 
    datastuff ds = new datastuff(); 

    // This is coming from a text box of my test form 
    ds.userid = textBox1.Text; 
    te.AddTodatastuff(ds); 

    // Save the changes 
    te.SaveChanges(true); 

    // This is not needed, only to make sure 
    te.Connection.Close(); 
} 

Dentro de SavingChanges inyectamos nuestro código para establecer la información de contexto de la conexión.

// Take my entity 
testEntities te = (testEntities)sender; 

// Get it's connection 
EntityConnection dc = (EntityConnection)te.Connection; 

// This is important! 
DbConnection storeConnection = dc.StoreConnection; 

// Create our command, which will call the userinit SP 
DbCommand command = storeConnection.CreateCommand(); 
command.CommandText = "userinit"; 
command.CommandType = CommandType.StoredProcedure; 

// Put the user id as the parameter 
command.Parameters.Add(new SqlParameter("userid", textBox1.Text)); 

// Execute the command 
command.ExecuteNonQuery(); 

Así que antes de guardar los cambios, se puede abrir la conexión del objeto, inyectar nuestro código (no cierre la conexión en esta parte!) Y guardar los cambios.

¡Y no olvides! Esto debe ampliarse para sus necesidades de registro, y debe ser probado bien, ¡porque esto muestra solo la posibilidad!

+0

La conexión de cierre es importante debido a 'System.ArgumentException: EntityConnection solo puede construirse con una DbConnection cerrada.' –

2

Hemos resuelto este problema de una manera diferente.

  1. Heredar una clase de su clase contenedora generada entidad
  2. Hacer el resumen clase de entidad de base. Puede hacerlo mediante una definición de clase parcial en un archivo separado
  3. En la clase heredada ocultar el método SavingChanges con su propio, usando la nueva palabra clave en la definición del método
  4. En sus SavingChanges método: a, abrir una conexión de la entidad b, ejecute el procedimiento almacenado de contexto de usuario con ebtityclient c, call base.SaveChanges() d, cierre la entityconnection

En su código, debe usar la clase heredada.

12

Gracias por apuntarme en la dirección correcta. Sin embargo, en mi caso, también necesito establecer la información de contexto cuando hago select statement, porque estoy consultando vistas que usan la información de contexto para controlar la seguridad de nivel de fila por usuario.

Encontré más fácil adjuntarlo al evento StateChanged de la conexión y solo observo el cambio de no abierto a abierto. Luego llamo al proceso que establece el contexto y funciona siempre, incluso si EF decide restablecer la conexión.

private int _contextUserId; 

public void SomeMethod() 
{ 
    var db = new MyEntities(); 
    db.Connection.StateChange += this.Connection_StateChange; 
    this._contextUserId = theCurrentUserId; 

    // whatever else you want to do 
} 

private void Connection_StateChange(object sender, StateChangeEventArgs e) 
{ 
    // only do this when we first open the connection 
    if (e.OriginalState == ConnectionState.Open || 
     e.CurrentState != ConnectionState.Open) 
     return; 

    // use the existing open connection to set the context info 
    var connection = ((EntityConnection) sender).StoreConnection; 
    var command = connection.CreateCommand(); 
    command.CommandText = "proc_ContextInfoSet"; 
    command.CommandType = CommandType.StoredProcedure; 
    command.Parameters.Add(new SqlParameter("ContextUserID", this._contextUserId)); 
    command.ExecuteNonQuery(); 
} 
+0

Me gusta este enfoque ya que también funcionará cuando se invoquen procedimientos almacenados desde EF. Presumiblemente agrega una sobrecarga adicional desafortunada en cada selección, ya que agrega una ejecución de proc de ida y vuelta. Sería bueno tener alguna forma de evitar eso, especialmente para gente que no necesita el conjunto CONTEXT_INFO para seleccionar. – Rory

+0

¿Sería posible crear una nueva 'DbExecutionStrategy' que ejecute este proceso en línea con cualquier otra ejecución, eliminando la ida y vuelta adicional? – Rory

+0

@Rory - TBH, tengo más de 5 años y no recuerdo ni siquiera escribirlo. Claramente lo hice, pero se ha ido de mi cerebro ahora. ;) –

2

simplemente forzar una ejecución del SET CONTEXT_INFO utilizando su DbContext o ObjectContext:

... 
FileMoverContext context = new FileMoverContext(); 
context.SetSessionContextInfo(Environment.UserName); 
... 
context.SaveChanges(); 

FileMoverContext hereda de DbContext y tiene un método SetSessionContextInfo. Esto es lo que mi SetSessionContextInfo (...) Se parece a:

public bool SetSessionContextInfo(string infoValue) 
{ 
    try 
    { 
     if (infoValue == null) 
     throw new ArgumentNullException("infoValue"); 

     string rawQuery = 
        @"DECLARE @temp varbinary(128) 
        SET @temp = CONVERT(varbinary(128), '"; 

     rawQuery = rawQuery + infoValue + @"'); 
        SET CONTEXT_INFO @temp"; 
     this.Database.ExecuteSqlCommand(rawQuery); 

     return true; 
    } 
    catch (Exception e) 
    { 
     return false; 
    } 
} 

Ahora que acaba de configurar un trigger que se puede acceder al CONTEXT_INFO() y establecer un campo de base de datos de usarlo.

1

tuve escenario algo similar, que resolví través de los siguientes pasos:

  1. En primer lugar crear un repositorio genérico para todas las operaciones CRUD como siguiente, que siempre es una buena aproximación. public class GenericRepository: IGenericRepository donde T: class

  2. Ahora escriba sus acciones como "public void Virtual update (T entityToUpdate)".

  3. Donde requirió el registro/Auditoría; simplemente llame a una función definida por el usuario de la siguiente manera "LogEntity (entityToUpdate," U ");".
  4. Consulte a continuación el archivo/clase pegado para definir la función "LogEntity". En esta función, en caso de actualización y eliminación obtendríamos la entidad anterior a través de la clave primaria para insertarla en la tabla de auditoría. Para identificar la clave principal y obtener su valor, utilicé la reflexión.

encontrar referencias de la clase completa a continuación:

public class GenericRepository<T> : IGenericRepository<T> where T : class 
{ 
    internal SampleDBContext Context; 
    internal DbSet<T> DbSet; 

    /// <summary> 
    /// Constructor to initialize type collection 
    /// </summary> 
    /// <param name="context"></param> 
    public GenericRepository(SampleDBContext context) 
    { 
     Context = context; 
     DbSet = context.Set<T>(); 
    } 

    /// <summary> 
    /// Get query on current entity 
    /// </summary> 
    /// <returns></returns> 
    public virtual IQueryable<T> GetQuery() 
    { 
     return DbSet; 
    } 

    /// <summary> 
    /// Performs read operation on database using db entity 
    /// </summary> 
    /// <param name="filter"></param> 
    /// <param name="orderBy"></param> 
    /// <param name="includeProperties"></param> 
    /// <returns></returns> 
    public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, 
              IOrderedQueryable<T>> orderBy = null, string includeProperties = "") 
    { 
     IQueryable<T> query = DbSet; 

     if (filter != null) 
     { 
      query = query.Where(filter); 
     } 

     query = includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Aggregate(query, (current, includeProperty) => current.Include(includeProperty)); 

     if (orderBy == null) 
      return query.ToList(); 
     else 
      return orderBy(query).ToList(); 
    } 

    /// <summary> 
    /// Performs read by id operation on database using db entity 
    /// </summary> 
    /// <param name="id"></param> 
    /// <returns></returns> 
    public virtual T GetById(object id) 
    { 
     return DbSet.Find(id); 
    } 

    /// <summary> 
    /// Performs add operation on database using db entity 
    /// </summary> 
    /// <param name="entity"></param> 
    public virtual void Insert(T entity) 
    { 
     //if (!entity.GetType().Name.Contains("AuditLog")) 
     //{ 
     // LogEntity(entity, "I"); 
     //} 
     DbSet.Add(entity); 
    } 

    /// <summary> 
    /// Performs delete by id operation on database using db entity 
    /// </summary> 
    /// <param name="id"></param> 
    public virtual void Delete(object id) 
    { 
     T entityToDelete = DbSet.Find(id); 
     Delete(entityToDelete); 
    } 

    /// <summary> 
    /// Performs delete operation on database using db entity 
    /// </summary> 
    /// <param name="entityToDelete"></param> 
    public virtual void Delete(T entityToDelete) 
    { 
     if (!entityToDelete.GetType().Name.Contains("AuditLog")) 
     { 
      LogEntity(entityToDelete, "D"); 
     } 

     if (Context.Entry(entityToDelete).State == EntityState.Detached) 
     { 
      DbSet.Attach(entityToDelete); 
     } 
     DbSet.Remove(entityToDelete); 
    } 

    /// <summary> 
    /// Performs update operation on database using db entity 
    /// </summary> 
    /// <param name="entityToUpdate"></param> 
    public virtual void Update(T entityToUpdate) 
    { 
     if (!entityToUpdate.GetType().Name.Contains("AuditLog")) 
     { 
      LogEntity(entityToUpdate, "U"); 
     } 
     DbSet.Attach(entityToUpdate); 
     Context.Entry(entityToUpdate).State = EntityState.Modified; 
    } 

    public void LogEntity(T entity, string action = "") 
    { 
     try 
     { 
      //*********Populate the audit log entity.********** 
      var auditLog = new AuditLog(); 
      auditLog.TableName = entity.GetType().Name; 
      auditLog.Actions = action; 
      auditLog.NewData = Newtonsoft.Json.JsonConvert.SerializeObject(entity); 
      auditLog.UpdateDate = DateTime.Now; 
      foreach (var property in entity.GetType().GetProperties()) 
      { 
       foreach (var attribute in property.GetCustomAttributes(false)) 
       { 
        if (attribute.GetType().Name == "KeyAttribute") 
        { 
         auditLog.TableIdValue = Convert.ToInt32(property.GetValue(entity)); 

         var entityRepositry = new GenericRepository<T>(Context); 
         var tempOldData = entityRepositry.GetById(auditLog.TableIdValue); 
         auditLog.OldData = tempOldData != null ? Newtonsoft.Json.JsonConvert.SerializeObject(tempOldData) : null; 
        } 

        if (attribute.GetType().Name == "CustomTrackAttribute") 
        { 
         if (property.Name == "BaseLicensingUserId") 
         { 
          auditLog.UserId = ValueConversion.ConvertValue(property.GetValue(entity).ToString(), 0); 
         } 
        } 
       } 
      } 

      //********Save the log in db.********* 
      new UnitOfWork(Context, null, false).AuditLogRepository.Insert(auditLog); 
     } 
     catch (Exception ex) 
     { 
      Logger.LogError(string.Format("Error occured in [{0}] method of [{1}]", Logger.GetCurrentMethod(), this.GetType().Name), ex); 
     } 
    } 
} 

CREATE TABLE [dbo].[AuditLog](
[AuditId] [BIGINT] IDENTITY(1,1) NOT NULL, 
[TableName] [nvarchar](250) NULL, 
[UserId] [int] NULL, 
[Actions] [nvarchar](1) NULL, 
[OldData] [text] NULL, 
[NewData] [text] NULL, 
[TableIdValue] [BIGINT] NULL, 
[UpdateDate] [datetime] NULL, 
CONSTRAINT [PK_DBAudit] PRIMARY KEY CLUSTERED 
(
[AuditId] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = 
OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 
Cuestiones relacionadas