Entity Framework 4.1 DbContext переопределяет SaveChanges для изменения свойств аудита

Я пытаюсь реализовать ограниченный журнал аудита изменений свойств для свойств в наборе классов. Я успешно узнал, как установить свойства CreateOn | ModifiedOn, но я не могу узнать, как "найти" измененное свойство.

Пример:

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

Итак, есть ли способ доступа к свойствам, которые были изменены с этим уровнем детализации в EF 4.1 DbContext?

Ответ 1

Очень, очень грубая идея:

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

У меня нет никакой подсказки, если бы это действительно работало подробно, но это то, что я хотел бы попробовать в качестве первого шага. Конечно, может быть больше одного свойства, которое изменилось, поэтому цикл и, возможно, несколько вызовов WriteAudit.

Отражающий материал внутри SaveChanges мог бы стать кошмаром производительности, хотя.

Edit

Возможно, лучше получить доступ к базовому ObjectContext. Тогда возможно что-то подобное:

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

Я использовал это сам в EF 4.0. Я не могу найти соответствующий метод для GetModifiedProperties (который является ключом, чтобы избежать кода отражения) в API DbContext.

Изменить 2

Важно. При работе с сущностями POCO код, указанный выше, должен вызвать DbContext.ChangeTracker.DetectChanges() в начале. Причина в том, что base.SaveChanges называется слишком поздно здесь (в конце метода). base.SaveChanges вызывает DetectChanges внутренне, но поскольку мы хотим проанализировать и зарегистрировать изменения раньше, мы должны вызвать DetectChanges вручную, чтобы EF мог найти все измененные свойства и правильно установить состояния в трекере изменения.

Возможны ситуации, когда код может работать без вызова DetectChanges, например, если методы DbContext/DbSet, такие как Add или Remove, используются после внесения последних изменений свойств, так как эти методы также вызывают DetectChanges внутренне. Но если, например, объект просто загружен из БД, изменяется несколько свойств, и затем вызывается этот производный SaveChanges, автоматическое обнаружение изменений не произойдет до base.SaveChanges, что в конечном итоге приведет к отсутствию записей журнала для модифицированных свойств.

Я обновил приведенный выше код.

Ответ 2

Вы можете использовать методы, предлагаемые Slauma, но вместо переопределения метода SaveChanges() вы можете обрабатывать событие SavingChanges для более простой реализации.

Ответ 3

Кажется, ответ Slauma не проверяет изменения внутренних свойств свойства сложного типа.

Если это проблема для вас, мой ответ здесь может оказаться полезным. Если свойство является сложным и оно изменилось, я сериализую все сложное свойство в запись журнала аудита. Это не самое эффективное решение, но это не так уж плохо, и он выполняет свою работу.

Ответ 4

Мне очень нравится решение Slauma. Обычно я предпочитаю отслеживать измененную таблицу и записывать первичный ключ tho. Это очень простой метод, который вы можете использовать для этого, вызывая getEntityKeys(entry)

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