Hay una necesidad de un cliente a un registro de cada cambio de datos a una tabla de registro con el usuario que realizó la modificación. La aplicación está utilizando SQL de un usuario para acceder a la base de datos, pero necesitamos de registro de la «real» id de usuario.

Podemos hacer esto en t-sql por la escritura de disparadores para cada tabla insert y update, y el uso de context_info para almacenar el id de usuario. Pasamos el id de usuario a un procedimiento almacenado, almacena el identificador de usuario en el contextinfo, y el gatillo podría utilizar esta información para escribir el registro filas a la tabla de registro.

No puedo encontrar el lugar o forma en dónde o cómo puedo hacer algo similar con EF. Así que el objetivo principal es: si hago un cambio en los datos a través de la EF, me gustaría registro de los datos exactos cambio de una tabla de una manera semiautomática (por lo que no desea comprobar para cada campo para el cambio, antes de guardar el objeto). Estamos utilizando EntitySQL.

Lamentablemente tenemos que pegar en SQL 2000, de modo que el cambio de datos de captura introducido en SQL2008 no es una opción (pero tal vez eso no es también el camino derecho para nosotros).

Las ideas, enlaces o puntos de partida?

[Editar]
Algunas notas: mediante el uso de ObjectContext.SavingChanges eventhandler, me puede llegar el punto en que se puede inyectar la instrucción SQL para inicializar el contextinfo. Sin embargo no puedo mezclar la EF y el estándar SQL. Así que puedo conseguir EntityConnection pero no puedo ejecutar una instrucción T-SQL usando. O puedo obtener la cadena de conexión de la EntityConnection y crear una SqlConnection basado en él, pero va a ser una conexión diferente, por lo que el contextinfo no afectará la operación de guardar hecha por la EF.

He intentado lo siguiente en el SavingChanges controlador:

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();

Error: El valor de EntityCommand.CommandText no es válido para un StoredProcedure comando.
Lo mismo con SqlParameter en lugar de EntityParameter: SqlParameter no puede ser utilizado.

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, atrapado para crear un puente entre el Marco de la Entidad y ADO.NET.
Si puedo conseguir que funcione, voy a publicar una prueba de concepto.

  • En serio que el usuario quiere que usted use EF y, a continuación, requiere seguir con SQL 2000?
  • Hay un interesante post sobre la auditoría de aquí, ¿qué te parece?
  • Seis años más tarde y todavía creo que esto es un gran acercamiento. Son los desencadenantes de la manera mejor para la auditoría de aplicación a nivel de código, y esto proporciona una ingeniosa manera de obtener ‘real’ información de usuario a la base de datos de la capa sin contaminar aplicación a nivel de código.
  • Claro agradable valoración crítica aquí davecallan.com/passing-userid-delete-trigger-entity-framework/# mediante una transacción enfoque para garantizar la misma conexión se utiliza.
InformationsquelleAutor Biri | 2008-11-17

8 Comentarios

  1. 12

    Cómo sobre el manejo de Contexto.SavingChanges?

    • Sí, eso es lo que me gustaría evitar. 🙂 Sería bueno para manejar todo esto bastante de forma automática. Ya tenemos el gatillo-generador para manejar el registro de parte. El eslabón perdido es que no podemos pasar el id de usuario de abajo el gatillo.
    • Usted no puede utilizar SavingChanges para establecer un ID de usuario en el contexto de la información? msdn.microsoft.com/en-us/library/ms187768.aspx
    • Oh, mierda. La solución más fácil y estábamos pensando en algo muy sofisticado. Gracias por abrir mis ojos.
    • Lo siento, tengo que revocar la aceptación de estado, porque no funciona. La conexión es cerrada (objetos separados) y si puedo abrir una nueva conexión y de relleno en el context_info, no afecta a la conexión abierta durante guardar. 🙁
    • Usted puede proporcionar su propia conexión para la EF para su uso. Ver: msdn.microsoft.com/en-us/library/bb738540.aspx
    • Sí, pero no puede ejecutar sql estándar declaraciones usando EntityConnection, y usted no puede convertir EntityConnection a SqlConnection, así que no puedo mezclar las dos técnicas.
    • EntityConnection es un DbConnection y por lo tanto, usted debería ser capaz de llamar CreateDbCommand. Yo no lo he probado, pero los documentos de la lista y no le dicen que no lo haga.
    • Sí, se puede, pero ¿qué se puede hacer con eso? No puedo ejecutar t-sql en. Voy a dar más muestras en la pregunta original.
    • Hmmm… Parece que usted tiene que utilizar StoreConnection (ver enlace). Se puede utilizar una proc? Como este? blogs.msdn.com/meek/archive/2008/03/26/…

  2. 13

    Gracias por me apunta en la dirección correcta. Sin embargo, en mi caso, yo también deberá establecer el contexto info al hacer las instrucciones select, porque soy consultar las vistas que utilizan el contexto de la información para el control de seguridad de nivel de fila por el usuario.

    He encontrado que es más fácil asociar al evento StateChanged de la conexión y el reloj justo para el cambio de la no-abrir para abrir. Entonces yo llamo el proc que establece el contexto y funciona todo el tiempo, 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();
    }
    • Me gusta este enfoque en el que se va a trabajar también cuando los procedimientos almacenados son llamados de EF. Presumiblemente se añade un desafortunado sobrecarga adicional en cada una de seleccionar, ya que añade un viaje de ida y vuelta proc ejecución. Sería bueno tener alguna manera para evitar que, sobre todo para la gente que no necesita el CONTEXT_INFO establecido para las mismas.
    • Tal vez sería posible crear un nuevo DbExecutionStrategy que ejecute este proc en línea con cualquier otra de las ejecuciones, la eliminación de la extra de ida y vuelta?
    • TBH, esto es más de 5 años de edad y no tengo recuerdo de que incluso la escritura. Claramente, yo lo hice, pero se ha ido de mi cerebro ahora. 😉
    • usted probablemente ha olvidado más de lo que la mayoría de la gente ha conocido 🙂
  3. 10

    Finalmente con Craig ayuda, aquí es una prueba de concepto. Se necesitan más pruebas, pero para el primer look de trabajo.

    Primero: he creado dos tablas, una para los datos 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: crear un desencadenador de inserción.

    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 spid[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 desencadenador para la actualización, que será un poco más sofisticado, ya que tiene que revisar cada campo para cambiar el contenido.

    La tabla de registro y el gatillo debe ser extendida para almacenar la tabla y el campo que es creado/modificado, pero espero que tuvo la idea.

    Tercero: crear un procedimiento almacenado que rellena el id de usuario para el SQL contexto info.

    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 el C# parte.

    Crear un proyecto y agregar un EDM para el proyecto. El EDM debe contener la datastuff de la tabla (o tablas se necesita para ver los cambios) y el SP.

    Ahora hacer algo con el objeto de entidad (por ejemplo, añadir una nueva datastuff objeto) y el gancho para el SavingChanges evento.

    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 la SavingChanges inyectamos nuestro código para establecer el contexto de la información 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 abre la conexión del objeto, inyectar nuestro código (no cierre la conexión en esta parte!) y guardar nuestros cambios.

    Y no se olvide! Esto necesita ser ampliada para sus necesidades de registro, y debe ser probado, porque este espectáculo sólo la posibilidad!

    • Cierre de conexión es importante, porque de System.ArgumentException: EntityConnection can only be constructed with a closed DbConnection.
  4. 3

    ¿Has probado añadiendo el procedimiento almacenado para su modelo de entidad?

    • Sí, lo hice. Craig señaló la dirección correcta, así que voy a publicar un POC siguiente.
  5. 2

    Simplemente forzar una ejecución del CONJUNTO de CONTEXT_INFO mediante su DbContext o ObjectContext:

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

    FileMoverContext hereda de DbContext y tiene un SetSessionContextInfo método.
    Aquí es lo que mi SetSessionContextInfo(…) parece:

    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 una base de datos de gatillo que puede acceder a la CONTEXT_INFO() y establecer un campo de base de datos utilizando.

  6. 2

    Tuvimos resolver este problema de una manera diferente.

    • Heredar una clase de la entidad generados clase de contenedor
    • Hacer la base de clase de entidad abstracta. Puede hacerlo por medio de un parcial de definición de clase en un archivo separado
    • En la clase heredada ocultar la SavingChanges método con su cuenta, utilizando la nueva palabra clave en la definición del método de
    • En su SavingChanges método:

      1. abrir una conexión de entidad
      2. ejecutar el contexto de usuario procedimiento almacenado con ebtityclient
      3. llamada base.SaveChanges()
      4. cerca de la entityconnection

    En tu código tienes que usar la clase heredada entonces.

  7. 0

    Tuve algo similar escenario, que me ha resuelto a través de los siguientes pasos:

    1. Primero crear un repositorio genérico para todas las operaciones CRUD como la siguiente, que siempre es un buen enfoque.
      clase pública GenericRepository : IGenericRepository donde T : clase

    2. Ahora escribir sus acciones como «public virtual void Update(T entityToUpdate)».

    3. Dondequiera que usted requiere registro y Auditoría; simplemente llamar a una función definida por el usuario de la siguiente manera «LogEntity(entityToUpdate, «U»);».
    4. Se refieren a continuación pega archivo/clase para definir «LogEntity» de la función. En esta función, en caso de actualizar y eliminar obtendríamos el viejo de la entidad a través de la clave principal para insertar en la tabla de auditoría. Para identificar la clave principal y obtener su valor he utilizado la reflexión.

    Encontrar la referencia 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]
  8. 0

    Esto es lo que he usado encontrado aquí he modificado porque no funcione

    private object GetPrimaryKeyValue(DbEntityEntry entry)
    {
    var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
    object o = objectStateEntry.EntityKey.EntityKeyValues[0].Value;
    return o;
    }
    private bool inExcludeList(string prop)
    {
    string[] excludeList = { "props", "to", "exclude" };
    return excludeList.Any(s => s.Equals(prop));
    }
    public int SaveChanges(User user, string UserId)
    {
    var modifiedEntities = ChangeTracker.Entries()
    .Where(p => p.State == EntityState.Modified).ToList();
    var now = DateTime.Now;
    foreach (var change in modifiedEntities)
    {
    var entityName = ObjectContext.GetObjectType(change.Entity.GetType()).Name;
    var primaryKey = GetPrimaryKeyValue(change);
    var DatabaseValues = change.GetDatabaseValues();
    foreach (var prop in change.OriginalValues.PropertyNames)
    {
    if(inExcludeList(prop))
    {
    continue;
    }
    string originalValue = DatabaseValues.GetValue<object>(prop)?.ToString();
    string currentValue = change.CurrentValues[prop]?.ToString();
    if (originalValue != currentValue)
    {
    ChangeLog log = new ChangeLog()
    {
    EntityName = entityName,
    PrimaryKeyValue = primaryKey.ToString(),
    PropertyName = prop,
    OldValue = originalValue,
    NewValue = currentValue,
    ModifiedByName = user.LastName + ", " + user.FirstName,
    ModifiedById = UserId,
    ModifiedBy = user,
    ModifiedDate = DateTime.Now
    };
    ChangeLogs.Add(log);
    }
    }
    }
    return base.SaveChanges();
    }
    public class ChangeLog 
    {
    public int Id { get; set; }
    public string EntityName { get; set; }
    public string PropertyName { get; set; }
    public string PrimaryKeyValue { get; set; }
    public string OldValue { get; set; }
    public string NewValue { get; set; }
    public string ModifiedByName { get; set; }
    [ForeignKey("ModifiedBy")]
    [DisplayName("Modified By")]
    public string ModifiedById { get; set; }
    public virtual User ModifiedBy { get; set; }
    [Column(TypeName = "datetime2")]
    public DateTime? ModifiedDate { get; set; }
    }

Dejar respuesta

Please enter your comment!
Please enter your name here