在我们的日常开发中,有时候需要记录数据库表中值的变化, 这时候我们通常会使用触发器或者使用关系型数据库中临时表(Temporal Table)或数据变更捕获(Change Data Capture)特性来记录数据库表中字段的值变化。原文的作者Gérald Barré讲解了如何使用Entity Freamwork Core上下文中的ChangeTracker来获取并保存实体的变化记录。
ChangeTracker是Entity Framework Core记录实体变更的核心对象(这一点和以前版本的Entity Framework一致)。当你使用Entity Framework Core进行获取实体对象、添加实体对象、删除实体对象、更新实体对象、附加实体对象等操作时,ChangeTracker都会记录下来对应的实体引用和对应的实体状态。
我们可以通过ChangeTracker.Entries()方法, 获取到当前上下文中使用的所有实体对象, 以及每个实体对象的状态属性State。
Entity Framework Core中可用的实体状态属性有以下几种
所以如果我们要记录实体的变更,只需要从ChangeTracker中取出所有Added, Deleted, Modified状态的实体, 并将其记录到一个日志表中即可。
我们以下面这个例子为例。
当前我们有一个顾客表Customer和一个日志表Audit, 其对应的实体对象及Entity Framework上下文如下:
    [Table("Audit")]
    public class Audit
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string TableName { get; set; }
        public DateTime DateTime { get; set; }
        public string KeyValues { get; set; }
        public string OldValues { get; set; }
        public string NewValues { get; set; }
    }    [Table("Customer")]
    public class Customer
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }    public class SampleContext : DbContext
    {
        public SampleContext()
        {
        }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Audit> Audits { get; set; }
    }我们希望当执行以下代码之后, 在Audit表中产生如下数据
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new SampleContext())
            {
                // Insert a row
                var customer = new Customer();
                customer.FirstName = "John";
                customer.LastName = "doe";
                context.Customers.Add(customer);
                context.SaveChangesAsync().Wait();
                // Update the first customer
                customer.LastName = "Doe";
                context.SaveChangesAsync().Wait();
                // Delete the customer
                context.Customers.Remove(customer);
                context.SaveChangesAsync().Wait();
            }
        }
    }
首先我们添加一个AuditEntry类, 来生成变更记录。
    public class AuditEntry
    {
        public AuditEntry(EntityEntry entry)
        {
            Entry = entry;
        }
        public EntityEntry Entry { get; }
        public string TableName { get; set; }
        public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();
        public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();
        public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();
        public List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();
        public bool HasTemporaryProperties => TemporaryProperties.Any();
        public Audit ToAudit()
        {
            var audit = new Audit();
            audit.TableName = TableName;
            audit.DateTime = DateTime.UtcNow;
            audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
            audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
            audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
            return audit;
        }
    }然后我们打开SampleContext.cs, 复写方法SaveChangeAsync代码如下。
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        await OnAfterSaveChanges(auditEntries);
        return result;
    }
    
    private List<AuditEntry> OnBeforeSaveChanges()
    {
        throw new NotImplementedException();
    }
    private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
    {
        throw new NotImplementedException();
    }OnBeforeSaveChange()和OnAfterSaveChanges。OnBeforeSaveChanges是用来获取所有需要记录的实体OnAfterSaveChanges是为了获得实体中数据库生成列的新值(例如自增列, 计算列)并持久化变更记录, 这一步必须放置在调用父类SaveChangesAsync之后,因为只有持久化之后,才能获取自增列和计算列的新值。OnBeforeSaveChange方法之后,OnAfterSaveChanges方法之前, 我们调用父类的SaveChangesAsync来保存实体变更。然后我们来修改OnBeforeSaveChanges方法, 代码如下
    private List<AuditEntry> OnBeforeSaveChanges()
    {
        ChangeTracker.DetectChanges();
        var auditEntries = new List<AuditEntry>();
        foreach (var entry in ChangeTracker.Entries())
        {
            if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                continue;
    
            var auditEntry = new AuditEntry(entry);
            auditEntry.TableName = entry.Metadata.Relational().TableName;
            auditEntries.Add(auditEntry);
    
            foreach (var property in entry.Properties)
            {
                if (property.IsTemporary)
                {
                    // value will be generated by the database, get the value after saving
                    auditEntry.TemporaryProperties.Add(property);
                    continue;
                }
    
                string propertyName = property.Metadata.Name;
                if (property.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[propertyName] = property.CurrentValue;
                    continue;
                }
    
                switch (entry.State)
                {
                    case EntityState.Added:
                        auditEntry.NewValues[propertyName] = property.CurrentValue;
                        break;
    
                    case EntityState.Deleted:
                        auditEntry.OldValues[propertyName] = property.OriginalValue;
                        break;
    
                    case EntityState.Modified:
                        if (property.IsModified)
                        {
                            auditEntry.OldValues[propertyName] = property.OriginalValue;
                            auditEntry.NewValues[propertyName] = property.CurrentValue;
                        }
                        break;
                }
            }
        }
    }ChangeTracker.DetectChanges()是强制上下文再做一次变更检查OnBeforeSaveChanges方法中,我们需要将Audit表的实体排除掉,否则会出现死循环Properties集合,里面记录的每个实体所有属性的状态, 如果某个属性被修改了,则该属性的IsModified是true.IsTemporary属性表明了该字段是不是数据库生成的。 我们将所有数据库生成的属性放到了TemplateProperties集合中,供OnAfterSaveChanges方法遍历Metadata.IsPrimaryKey()方法来获得当前字段是不是主键字段最后我们修改一下OnAfterSaveChanges, 代码如下
    private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
    {
        if (auditEntries == null || auditEntries.Count == 0)
            return Task.CompletedTask;
        foreach (var auditEntry in auditEntries)
        {
            // Get the final value of the temporary properties
            foreach (var prop in auditEntry.TemporaryProperties)
            {
                if (prop.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
                }
                else
                {
                    auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
                }
            }
            // Save the Audit entry
            Audits.Add(auditEntry.ToAudit());
        }
        return SaveChangesAsync();
    }OnBeforeSaveChanges中,我们记录下了当前实体所有需要数据库生成的属性。 在调用父类的SaveChangesAsync方法, 我们可以获取通过property的CurrentValue属性获得到这些数据库生成属性的新值OnBeforeSaveChanges中就可以生成这个DateTime让时间更精确一些)Entitiy Framework Core中使用ChangeTracker持久化实体修改历史
原文:https://www.cnblogs.com/lwqlun/p/9693970.html