如何在 .SaveChanges() 期间首先使用 EF 代码记录所有实体更改?

Posted

技术标签:

【中文标题】如何在 .SaveChanges() 期间首先使用 EF 代码记录所有实体更改?【英文标题】:How can I log all entities change, during .SaveChanges() using EF code first? 【发布时间】:2013-07-28 02:20:26 【问题描述】:

我首先使用 EF 代码。我正在为我的所有存储库使用一个基本存储库,以及一个注入到存储库的IUnitofWork

public interface IUnitOfWork : IDisposable

    IDbSet<TEntity> Set<TEntity>() where TEntity : class;
    int SaveChanges();


public class BaseRepository<T> where T : class

    protected readonly DbContext _dbContext;
    protected readonly IDbSet<T> _dbSet;


    public BaseRepository(IUnitOfWork uow)
    
        _dbContext = (DbContext)uow;
        _dbSet = uow.Set<T>();
    
    //other methods
   

例如我的OrderRepository 是这样的:

class OrderRepository: BaseRepository<Order>

    IUnitOfWork _uow;
    IDbSet<Order> _order;

    public OrderRepository(IUnitOfWork uow)
        : base(uow)
    
        _uow = uow;
        _order = _uow.Set<Order>();
    
    //other methods

而我是这样使用的:

public void Save(Order order)

        using (IUnitOfWork uow = new MyDBContext())
        
            OrderRepository repository = new OrderRepository(uow); 
            try
            
               repository.ApplyChanges<Order>(order);    
               uow.SaveChanges();
              

         
     

有没有办法在.SaveChanges() 期间记录所有实体的更改历史记录(包括它们的导航属性)?我想记录原始值(在保存之前)和更改的值(在保存之后)。

【问题讨论】:

“包括他们的导航属性”是什么意思?这可能会导致您的大部分数据库被写入您的日志... 另外,您能否更准确地说明要记录哪些更改?现在的措辞方式看起来您只对保存期间所做的更改(即IDENTITY 列和时间戳)感兴趣,但我怀疑您的意思是记录repository.ApplyChanges 之前和之后的值。对吗? @SteveRuble:是的,我的意思是记录之前和之后的值,也用于“包括他们的导航属性”,例如当我保存订单时,我想记录 Order.Customer.Name 值保存前后。 【参考方案1】:

您可以通过DbContext.ChangeTracker 获取所有更改实体的前后值。不幸的是,API 有点冗长:

var changeInfo = context.ChangeTracker.Entries()
            .Where (t => t.State == EntityState.Modified)
            .Select (t => new 
                Original = t.OriginalValues.PropertyNames.ToDictionary (pn => pn, pn => t.OriginalValues[pn]),
                Current = t.CurrentValues.PropertyNames.ToDictionary (pn => pn, pn => t.CurrentValues[pn]),
            );

如果您的日志记录需要,您可以修改它以包含实体类型等内容。 DbPropertyValues(OriginalValues 和 CurrentValues 的类型)上还有一个 ToObject() 方法,如果您已经有方法记录整个对象,则可以调用该方法,尽管从该方法返回的对象不会填充其导航属性。

如果根据您的要求更有意义的话,您还可以修改该代码以通过删除 Where 子句来获取上下文中的所有实体。

【讨论】:

谢谢,正如您所说的“从该方法返回的对象不会填充其导航属性”,但是导航属性对我很重要,您知道有什么方法可以做到这一点吗? @Masoud,将导航属性记录为“Order.Customer.Name=Value”是否重要,还是仅将 Customer.Name 的值包含在记录的信息中就足够了?如果你想要第一个选项,你将需要编写一些非常复杂的东西来以你想要的格式序列化你的实体;如果你想要第二个,我的例子应该适合你。 你不能把 ToObject 返回的对象附加到一个上下文中,让上下文修复所有的实体关系吗?【参考方案2】:

我已经覆盖了默认的 SaveChanges 方法来记录实体中添加/更新/删除的更改。虽然它不包括导航属性更改。基于这篇文章:Using entity framework for auditing

public int SaveChanges(string userId)
    
        int objectsCount;

        List<DbEntityEntry> newEntities = new List<DbEntityEntry>();

        // Get all Added/Deleted/Modified entities (not Unmodified or Detached)
        foreach (var entry in this.ChangeTracker.Entries().Where
            (x => (x.State == System.Data.EntityState.Added) ||
                (x.State == System.Data.EntityState.Deleted) ||
                (x.State == System.Data.EntityState.Modified)))
        
            if (entry.State == System.Data.EntityState.Added)
            
                newEntities.Add(entry);
            
            else
            
                // For each changed record, get the audit record entries and add them
                foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId))
                
                    this.AuditLogs.Add(changeDescription);
                
            
        

        // Default save changes call to actually save changes to the database
        objectsCount = base.SaveChanges();

        // We don't have recordId for insert statements that's why we need to call this method again.
        foreach (var entry in newEntities)
        
            // For each changed record, get the audit record entries and add them
            foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId, true))
            
                this.AuditLogs.Add(changeDescription);
            

            // TODO: Think about performance here. We are calling db twice for one insertion.
            objectsCount += base.SaveChanges();
        

        return objectsCount;
    

    #endregion

    #region Helper Methods

    /// <summary>
    /// Helper method to create record description for Audit table based on operation done on dbEntity
    /// - Insert, Delete, Update
    /// </summary>
    /// <param name="dbEntity"></param>
    /// <param name="userId"></param>
    /// <returns></returns>
    private List<AuditLog> GetAuditRecordsForEntity(DbEntityEntry dbEntity, string userId, bool insertSpecial = false)
    
        List<AuditLog> changesCollection = new List<AuditLog>();

        DateTime changeTime = DateTime.Now;

        // Get Entity Type Name.
        string tableName1 = dbEntity.GetTableName();

        // http://***.com/questions/2281972/how-to-get-a-list-of-properties-with-a-given-attribute
        // Get primary key value (If we have more than one key column, this will need to be adjusted)
        string primaryKeyName = dbEntity.GetAuditRecordKeyName();

        int primaryKeyId = 0;
        object primaryKeyValue;

        if (dbEntity.State == System.Data.EntityState.Added || insertSpecial)
        
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName, true);

            if(primaryKeyValue != null)
            
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
                            

            // For Inserts, just add the whole record
            // If the dbEntity implements IDescribableEntity,
            // use the description from Describe(), otherwise use ToString()
            changesCollection.Add(new AuditLog()
                    
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_ADD,
                        TableName = tableName1,
                        RecordId = primaryKeyId,  // Again, adjust this if you have a multi-column key
                        ColumnName = "ALL",    // To show all column names have been changed
                        NewValue = (dbEntity.CurrentValues.ToObject() is IAuditableEntity) ?
                                        (dbEntity.CurrentValues.ToObject() as IAuditableEntity).Describe() :
                                        dbEntity.CurrentValues.ToObject().ToString()
                    
                );
        

        else if (dbEntity.State == System.Data.EntityState.Deleted)
        
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);

            if (primaryKeyValue != null)
            
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
            

            // With deletes use whole record and get description from Describe() or ToString()
            changesCollection.Add(new AuditLog()
                    
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_DELETE,
                        TableName = tableName1,
                        RecordId = primaryKeyId,
                        ColumnName = "ALL",
                        OriginalValue = (dbEntity.OriginalValues.ToObject() is IAuditableEntity) ?
                                    (dbEntity.OriginalValues.ToObject() as IAuditableEntity).Describe() :
                                    dbEntity.OriginalValues.ToObject().ToString()
                    );
        

        else if (dbEntity.State == System.Data.EntityState.Modified)
        
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);

            if (primaryKeyValue != null)
            
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
            

            foreach (string propertyName in dbEntity.OriginalValues.PropertyNames)
            
                // For updates, we only want to capture the columns that actually changed
                if (!object.Equals(dbEntity.OriginalValues.GetValue<object>(propertyName),
                        dbEntity.CurrentValues.GetValue<object>(propertyName)))
                
                    changesCollection.Add(new AuditLog()
                    
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_MODIFY,
                        TableName = tableName1,
                        RecordId = primaryKeyId,
                        ColumnName = propertyName,
                        OriginalValue = dbEntity.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntity.OriginalValues.GetValue<object>(propertyName).ToString(),
                        NewValue = dbEntity.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntity.CurrentValues.GetValue<object>(propertyName).ToString()
                    
                        );
                
            
        


        // Otherwise, don't do anything, we don't care about Unchanged or Detached entities
        return changesCollection;
    

【讨论】:

你是什么意思它不包括导航属性的变化? EF 跟踪所有对象的更改。请澄清。【参考方案3】:

你用额外的要求吓跑了人们

包括他们的导航属性

这只是一个简单的练习。 如果这很重要,您应该使用代码管理/跟踪跨引用的更改。

这是一个涵盖该主题的示例 Undo changes in entity framework entities

这里有一个样本做接近你想要的 undo changes 它可以很容易地转换为在其他地方加载前后图像。

给定调用 DetectChanges 后的 ObjectState 条目,您可以通过实体选项实现简单的实体。并根据 UOW。但是导航/引用版本使这变得非常复杂,因为您提出了要求。

编辑:如何访问更改列表

     public class  Repository<TPoco>
     /....
     public DbEntityEntry<T> Entry(T entity)  return Context.Entry(entity); 

     public virtual IList<ChangePair> GetChanges(object poco) 

        var changes = new List<ObjectPair>();
        var thePoco = (TPoco) poco;

        foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) 
            var curr = Entry(thePoco).CurrentValues[propName];
            var orig = Entry(thePoco).OriginalValues[propName];
            if (curr != null && orig != null) 
                if (curr.Equals(orig)) 
                    continue;
                
            
            if (curr == null && orig == null) 
                continue;
            
            var aChangePair = new ChangePair Key = propName, Current = curr, Original = orig;
            changes.Add(aChangePair);
        
        return changes;
    
    ///...  partial repository shown
     
// FYI the simple return structure

public class ChangePair 
    public string Key  get; set; 
    public object Original  get; set; 
    public object Current  get; set; 
 

【讨论】:

谢谢,菲尔,但你建议的解决方案假设我有属性名称,当我想要记录所有实体的所有属性的更改时,虽然我认为使用反射,我可以做到,但是反射存在效率问题(速度慢)。【参考方案4】:

DbContext 具有 ChangeTracker 属性。 您可以在您的上下文中覆盖 .SaveChanges() 并记录更改。 我不认为实体框架可以为您做到这一点。您可能必须直接检测模型类中的更改。

【讨论】:

【参考方案5】:

我已经扩展了 Steve 的答案,以提供对 Changed、Added 和 Deleted 实体的检查,并以合理的方式打印它们。

(我的用例是确保在处理 DbContext 实例之前没有未保存的更改,但此检查可以在任何时候进行)


/// <summary>Helper method that checks whether the DbContext had any unsaved changes before it was disposed.</summary>
private void CheckForUnsavedChanges(DbContext dbContext)

    try
    
        List<DbEntityEntry> changedEntityEntries = dbContext.ChangeTracker.Entries()
            .Where(t => t.State != EntityState.Unchanged && t.State != EntityState.Detached).ToList();
        if (!changedEntityEntries.Any())
            return;
        throw new Exception("Detected that there were unsaved changes made using a DbContext. This could be due to a missing call to `.SaveChanges()` or possibly " +
            "some read-only operations that modified the returned entities (in which case you might wish to use `.AsNoTracking()` in your query). Changes:\n    " +
            String.Join("\n    ", changedEntityEntries.Select(entry => $"entry.Entity.GetType() entry.State:\n        " + String.Join("\n        ",
            entry.State == EntityState.Modified ? entry.CurrentValues.PropertyNames
                // Only output properties whose values have changed (and hope they have a good ToString() implementation) 
                .Where(pn => entry.OriginalValues?[pn] != entry.CurrentValues[pn])
                .Select(pn => $"pn (entry.OriginalValues?[pn] -> entry.CurrentValues[pn])") :
            // Added or Deleted entities are output in their entirety
            entry.State == EntityState.Added ? entry.CurrentValues.PropertyNames.Select(pn => $"pn = entry.CurrentValues[pn]") :
         /* entry.State == EntityState.Deleted ? */ entry.CurrentValues.PropertyNames.Select(pn => $"pn = entry.OriginalValues[pn]")))));
    
    catch (Exception ex)
    
        _logger.Error("Issue encountered when checking for unsaved changes.", ex);
    

【讨论】:

以上是关于如何在 .SaveChanges() 期间首先使用 EF 代码记录所有实体更改?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过覆盖 SaveChanges() 的默认行为来执行多个 SaveChanges() 调用?

通过实体框架更新时如何绕过唯一键约束(使用 dbcontext.SaveChanges())

如何更改 DbContext.SaveChanges() 的条目值

如何在我的程序中记录从 DbContext.SaveChanges() 生成的 SQL? [复制]

如何使用 dbcontext SaveChanges 将修改后的记录推送到数据库?

如何首先使用实体​​框架代码更新一行?