2011-05-27 13 views
39

Estoy tratando de implementar un 'registro de auditoría' restringido de cambios de propiedad a propiedades en un conjunto de clases. He descubierto con éxito cómo configurar las propiedades de tipo CreatedOn | ModifiedOn, pero no puedo encontrar cómo 'encontrar' la propiedad que se ha modificado.Entity Framework 4.1 DbContext Reemplazar SaveChanges para auditar el cambio de propiedad

Ejemplo:

public class TestContext : DbContext 
{ 
    public override int SaveChanges() 
    { 
     var utcNowAuditDate = DateTime.UtcNow; 
     var changeSet = ChangeTracker.Entries<IAuditable>(); 
     if (changeSet != null) 
      foreach (DbEntityEntry<IAuditable> dbEntityEntry in changeSet) 
      { 

       switch (dbEntityEntry.State) 
       { 
        case EntityState.Added: 
         dbEntityEntry.Entity.CreatedOn = utcNowAuditDate; 
         dbEntityEntry.Entity.ModifiedOn = utcNowAuditDate; 
         break; 
        case EntityState.Modified: 
         dbEntityEntry.Entity.ModifiedOn = utcNowAuditDate; 
         //some way to access the name and value of property that changed here 
         var changedThing = SomeMethodHere(dbEntityEntry); 
         Log.WriteAudit("Entry: {0} Origianl :{1} New: {2}", changedThing.Name, 
             changedThing.OrigianlValue, changedThing.NewValue) 
         break; 
       } 
      } 
     return base.SaveChanges(); 
    } 
} 

Entonces, ¿hay una manera de acceder a la propiedad que cambió con este nivel de detalle en EF 4.1 DbContext?

Respuesta

44

idea muy, muy áspera:

foreach (var property in dbEntityEntry.Entity.GetType().GetProperties()) 
{ 
    DbPropertyEntry propertyEntry = dbEntityEntry.Property(property.Name); 
    if (propertyEntry.IsModified) 
    { 
     Log.WriteAudit("Entry: {0} Original :{1} New: {2}", property.Name, 
      propertyEntry.OriginalValue, propertyEntry.CurrentValue); 
    } 
} 

no tengo ni idea de si esto realmente trabajar en detalle, pero esto es algo que me gustaría probar como un primer paso. Por supuesto, podría haber más de una propiedad que haya cambiado, por lo tanto, el bucle y quizás varias llamadas de WriteAudit.

El material de reflexión dentro de SaveChanges podría convertirse en una pesadilla de rendimiento.

Editar

Tal vez sea mejor para acceder a la ObjectContext subyacente. Entonces algo como esto es posible:

public class TestContext : DbContext 
{ 
    public override int SaveChanges() 
    { 
     ChangeTracker.DetectChanges(); // Important! 

     ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext; 

     List<ObjectStateEntry> objectStateEntryList = 
      ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added 
                 | EntityState.Modified 
                 | EntityState.Deleted) 
      .ToList(); 

     foreach (ObjectStateEntry entry in objectStateEntryList) 
     { 
      if (!entry.IsRelationship) 
      { 
       switch (entry.State) 
       { 
        case EntityState.Added: 
         // write log... 
         break; 
        case EntityState.Deleted: 
         // write log... 
         break; 
        case EntityState.Modified: 
        { 
         foreach (string propertyName in 
            entry.GetModifiedProperties()) 
         { 
          DbDataRecord original = entry.OriginalValues; 
          string oldValue = original.GetValue(
           original.GetOrdinal(propertyName)) 
           .ToString(); 

          CurrentValueRecord current = entry.CurrentValues; 
          string newValue = current.GetValue(
           current.GetOrdinal(propertyName)) 
           .ToString(); 

          if (oldValue != newValue) // probably not necessary 
          { 
           Log.WriteAudit(
            "Entry: {0} Original :{1} New: {2}", 
            entry.Entity.GetType().Name, 
            oldValue, newValue); 
          } 
         } 
         break; 
        } 
       } 
      } 
     } 
     return base.SaveChanges(); 
    } 
} 

He usado esto en EF 4.0. No puedo encontrar el método correspondiente en GetModifiedProperties (que es la clave para evitar el código de reflexión) en la API DbContext.

Editar 2

Importante: Cuando se trabaja con entidades POCO el código anterior tiene que llamar DbContext.ChangeTracker.DetectChanges() al principio. El motivo es que base.SaveChanges se llama demasiado tarde aquí (al final del método). base.SaveChanges llama a DetectChanges internamente, pero como queremos analizar y registrar los cambios antes, debemos llamar al DetectChanges manualmente para que EF pueda encontrar todas las propiedades modificadas y establecer los estados en el rastreador de cambios correctamente.

hay posibles situaciones en las que el código puede funcionar sin llamar DetectChanges, por ejemplo, si se utilizan métodos DbContext/DbSet como AddRemove o después de las últimas modificaciones de las propiedades se hacen ya que estos métodos también se llaman DetectChanges internamente. Pero si, por ejemplo, una entidad acaba de cargarse desde DB, se cambian algunas propiedades y luego se llama a este derivado SaveChanges, la detección automática de cambios no ocurriría antes del base.SaveChanges, lo que finalmente daría como resultado entradas de registro faltantes para las propiedades modificadas.

He actualizado el código anterior en consecuencia.

+0

Esto funciona, pero parece que todas las propiedades se modifican de acuerdo con esto. Investigará más, pero está bien para la pregunta original. Creo que esto me da lo que necesito. –

+0

Esto no funciona para mí. Los viejos valores siempre están vacíos. ¿Funciona esto solo para POCO y 4.1 y más? – Jonx

+0

@Jonx: Sí, es EF 4.1 ('DbContext') y POCO. Sin embargo, puede hacer algo similar para <= EF 4.0 ('ObjectContext'), la única diferencia es que no sobrescribe' SaveChanges' sino que implementa un controlador de eventos ('OnSavingChanges' o similar, no recuerdo exactamente). – Slauma

7

Puede utilizar los métodos que Slauma sugiere, pero en lugar de anular el método SaveChanges(), puede manejar el evento SavingChanges para una implementación mucho más sencilla.

+0

Oh, buen punto. En realidad, mi fragmento de código anterior proviene de un controlador 'SavingChanges', lo olvidé. Pero, ¿por qué hace las cosas más fáciles? ¿No es básicamente lo mismo que tienes que hacer para escribir el registro? – Slauma

+0

Hace que sea más fácil si necesita agregar algo, en lugar de modificar su anulación, puede simplemente agregar un controlador. Tampoco tiene que preocuparse por transmitir la funcionalidad original, que creo que falta en su ejemplo. Los objetos nunca se salvarían. Si simplemente escribió un controlador, no tendría que preocuparse por eso. – Jay

+0

Es cierto, gracias, me olvidé de llamar a 'SaveChanges' de la clase base (agregado ahora). (Fue porque copié las partes principales del fragmento de mi controlador real 'SavingChanges'.) – Slauma

1

Parece que la respuesta de Slauma no audita los cambios en las propiedades internas de una propiedad de tipo complejo.

Si esto es un problema para usted, mi respuesta here podría ser útil. Si la propiedad es compleja y ha cambiado, serializo toda la propiedad compleja en el registro de auditoría. No es la solución más eficiente, pero no es tan mala y hace el trabajo bien.

+0

Eso podría deberse a que los tipos complejos no tienen proxy, ref https://blog.oneunicorn.com/2012/03/13/secrets-of-detectchanges-part-4-binary-properties-and-complex-types/ –

1

Me gusta mucho la solución de Slauma. Por lo general, prefiero seguir la tabla modificada y registro la (s) clave (s) primaria (s). Este es un método muy simple que puede utilizar para hacer eso, llamando getEntityKeys(entry)

public static string getEntityKeys(ObjectStateEntry entry) 
    { 
     return string.Join(", ", entry.EntityKey.EntityKeyValues 
           .Select(x => x.Key + "=" + x.Value)); 
    } 
Cuestiones relacionadas