呼图壁网站建设,做网站销售提成怎么算,长春高端品牌网站建设,企业管理官网登录入口EF Core 数据变更自动审计设计Intro有的时候我们需要知道每个数据表的变更记录以便做一些数据审计#xff0c;数据恢复以及数据同步等之类的事情#xff0c; EF 自带了对象追踪#xff0c;使得我们可以很方便的做一些审计工作#xff0c;每次变更发生了什么变化都变得很清晰… EF Core 数据变更自动审计设计Intro有的时候我们需要知道每个数据表的变更记录以便做一些数据审计数据恢复以及数据同步等之类的事情 EF 自带了对象追踪使得我们可以很方便的做一些审计工作每次变更发生了什么变化都变得很清晰于是就基于 EF 封装了一层数据变更自动审计使用效果测试代码private static void AutoAuditTest()
{// 审计配置AuditConfig.Configure(builder {builder// 配置操作用户获取方式.WithUserIdProvider(EnvironmentAuditUserIdProvider.Instance.Value)//.WithUnModifiedProperty() // 保存未修改的属性,默认只保存发生修改的属性// 保存更多属性.EnrichWithProperty(MachineName, Environment.MachineName).EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName)// 保存到自定义的存储.WithStoreAuditFileStore().WithStoreAuditFileStore(logs.log)// 忽略指定实体.IgnoreEntityAuditRecord()// 忽略指定实体的某个属性.IgnorePropertyTestEntity(t t.CreatedAt)// 忽略所有属性名称为 CreatedAt 的属性.IgnoreProperty(CreatedAt);});DependencyResolver.TryInvokeServiceTestDbContext(dbContext {dbContext.Database.EnsureDeleted();dbContext.Database.EnsureCreated();var testEntity new TestEntity(){Extra new { Name Tom }.ToJson(),CreatedAt DateTimeOffset.UtcNow,};dbContext.TestEntities.Add(testEntity);dbContext.SaveChanges();testEntity.CreatedAt DateTimeOffset.Now;testEntity.Extra new { Name Jerry }.ToJson();dbContext.SaveChanges();dbContext.Remove(testEntity);dbContext.SaveChanges();var testEntity1 new TestEntity(){Extra new { Name Tom1 }.ToJson(),CreatedAt DateTimeOffset.UtcNow,};dbContext.TestEntities.Add(testEntity1);var testEntity2 new TestEntity(){Extra new { Name Tom2 }.ToJson(),CreatedAt DateTimeOffset.UtcNow,};dbContext.TestEntities.Add(testEntity2);dbContext.SaveChanges();});DependencyResolver.TryInvokeServiceTestDbContext(dbContext {dbContext.Remove(new TestEntity(){Id 2});dbContext.SaveChanges();});// disable auditAuditConfig.DisableAudit();
}
查看审计记录信息可以看到每次数据变更都会被记录下来 CreatedAt 没有记录是因为上面配置的忽略 CreatedAt 属性信息的记录。这里的 TableName 属性名称和 Entity 定义的不同是为了测试列名和属性名称不一致的情况实际记录的是数据库里的表名称和列名称之所以这样设计考虑的是可能多个应用使用同一张表但是不同的应用里可能使用的 Entity 和 Property 都不同所以统一使用了数据库的表名称和字段名称。OperationType是一个枚举1是新增2是删除3是修改。Extra 列对应的就是我们自定义的增加的审计属性UpdatedBy 是我们配置的 UserIdProvider 所提供的操作用户的信息值得注意的是最后一条变更记录这条数据的删除没有经过数据库查询直接删除的EF 不知道原本的除了主键之外的信息所以记录的原始信息可能不准确不过还是知道谁删除的这一条数据对比之前的变更还是可以满足需求的。实现原理实现的原理是基于 EF 的内置的 Change Tracking 来实现的EF 每次 SaveChanges 之前都会检测变更每条变更的记录都会记录变更前的属性值以及变更之后的属性值因此我们可以在 SaveChanges 之前记录变更前后的属性对于数据库生成的值如 SQL Server 里的自增主键在保存之前属性的会被标记为 IsTemporary 保存成功之后会自动更新在保存之后可以获取到数据库生成的值。实现代码首先实现一个 DbContextBase重写 SaveChanges 和 SaveChangesAsync 方法增加BeforeSaveChanges 和 AfterSaveChanges 方法用于处理我们要自定义的保存之前和保存之后的逻辑。public abstract class DbContextBase : DbContext
{protected DbContextBase(){}protected DbContextBase(DbContextOptions dbContextOptions) : base(dbContextOptions){}protected virtual Task BeforeSaveChanges() Task.CompletedTask;protected virtual Task AfterSaveChanges() Task.CompletedTask;public override int SaveChanges(){BeforeSaveChanges().Wait();var result base.SaveChanges();AfterSaveChanges().Wait();return result;}public override async Taskint SaveChangesAsync(CancellationToken cancellationToken default){await BeforeSaveChanges();var result await base.SaveChangesAsync(cancellationToken);await AfterSaveChanges();return result;}
接着来实现一个用来自动审计的 AuditDbContextBase核心代码如下public abstract class AuditDbContextBase : DbContextBase
{protected AuditDbContextBase(){}protected AuditDbContextBase(DbContextOptions dbContextOptions) : base(dbContextOptions){}protected ListAuditEntry AuditEntries { get; set; }protected override Task BeforeSaveChanges(){AuditEntries new ListAuditEntry();foreach (var entityEntry in ChangeTracker.Entries()){if (entityEntry.State EntityState.Detached || entityEntry.State EntityState.Unchanged){continue;}AuditEntries.Add(new AuditEntry(entityEntry));}return Task.CompletedTask;}protected override async Task AfterSaveChanges(){if (null ! AuditEntries AuditEntries.Count 0){foreach (var auditEntry in AuditEntries){// update TemporaryPropertiesif (auditEntry.TemporaryProperties ! null auditEntry.TemporaryProperties.Count 0){foreach (var temporaryProperty in auditEntry.TemporaryProperties){var colName temporaryProperty.Metadata.GetColumnName();if (temporaryProperty.Metadata.IsPrimaryKey()){auditEntry.KeyValues[colName] temporaryProperty.CurrentValue;}switch (auditEntry.OperationType){case OperationType.Add:auditEntry.NewValues[colName] temporaryProperty.CurrentValue;break;case OperationType.Delete:auditEntry.OriginalValues[colName] temporaryProperty.OriginalValue;break;case OperationType.Update:auditEntry.OriginalValues[colName] temporaryProperty.OriginalValue;auditEntry.NewValues[colName] temporaryProperty.CurrentValue;break;}}// set to nullauditEntry.TemporaryProperties null;}}// ... save audit entries}
}
此时我们已经可以实现自动的审计处理了但是在实际业务处理的过程中往往我们还会有更多的需求比如上面的实现还没有加入更新人不知道是由谁来操作的有些字段可能不希望被记录下来或者有些表不要记录还有我们向增加一些自定义的属性比如多个应用操作同一个数据库表的时候我们可能希望记录下来是哪一个用户通过哪一个应用来更新的等等所以之前上面的实现还是不能够实际应用的于是我又在上面的基础上增加了一些配置以及扩展使得自动审计扩展性更好可定制性更强。扩展UserIdProvider我们可以通过 UserIdProvider 来实现操作用户信息的获取默认提供两个实现定义如下public interface IAuditUserIdProvider
{string GetUserId();
}
默认实现// 获取 Environment.UserName
public class EnvironmentAuditUserIdProvider : IAuditUserIdProvider
{private EnvironmentAuditUserIdProvider(){}public static LazyEnvironmentAuditUserIdProvider Instance new LazyEnvironmentAuditUserIdProvider(() new EnvironmentAuditUserIdProvider(), true);public string GetUserId() Environment.UserName;
}
// 获取 Thread.CurrentPrincipal.Identity.Name
public class ThreadPrincipalUserIdProvider : IAuditUserIdProvider
{public static LazyThreadPrincipalUserIdProvider Instance new LazyThreadPrincipalUserIdProvider(() new ThreadPrincipalUserIdProvider(), true);private ThreadPrincipalUserIdProvider(){}public string GetUserId() Thread.CurrentPrincipal?.Identity?.Name;
}
当然如果是 asp.net core 你也可以实现相应的基于 HttpContext 实现的 UserIdProviderFilters基于我们可能希望忽略一些实体或属性记录所以有必要增加 Filter 的记录基于实体的 Filter: FuncEntityEntry,bool基于属性的 Filter: FuncEntityEntry,PropertyEntry,bool为了使用方便定义了一些扩展方法public static IAuditConfigBuilder IgnoreEntity(this IAuditConfigBuilder configBuilder, Type entityType)
{configBuilder.WithEntityFilter(entityEntry entityEntry.Entity.GetType() ! entityType);return configBuilder;
}
public static IAuditConfigBuilder IgnoreEntityTEntity(this IAuditConfigBuilder configBuilder) where TEntity : class
{configBuilder.WithEntityFilter(entityEntry entityEntry.Entity.GetType() ! typeof(TEntity));return configBuilder;
}
public static IAuditConfigBuilder IgnoreTable(this IAuditConfigBuilder configBuilder, string tableName)
{configBuilder.WithEntityFilter(entityEntry entityEntry.Metadata.GetTableName() ! tableName);return configBuilder;
}
public static IAuditConfigBuilder WithEntityFilter(this IAuditConfigBuilder configBuilder, FuncEntityEntry, bool filterFunc)
{configBuilder.WithEntityFilter(filterFunc);return configBuilder;
}
public static IAuditConfigBuilder IgnorePropertyTEntity(this IAuditConfigBuilder configBuilder, ExpressionFuncTEntity, object propertyExpression) where TEntity : class
{var propertyName propertyExpression.GetMemberName();configBuilder.WithPropertyFilter(propertyEntry propertyEntry.Metadata.Name ! propertyName);return configBuilder;
}
public static IAuditConfigBuilder IgnoreProperty(this IAuditConfigBuilder configBuilder, string propertyName)
{configBuilder.WithPropertyFilter(propertyEntry propertyEntry.Metadata.Name ! propertyName);return configBuilder;
}
public static IAuditConfigBuilder IgnoreColumn(this IAuditConfigBuilder configBuilder, string columnName)
{configBuilder.WithPropertyFilter(propertyEntry propertyEntry.Metadata.GetColumnName() ! columnName);return configBuilder;
}
public static IAuditConfigBuilder IgnoreColumn(this IAuditConfigBuilder configBuilder, string tableName, string columnName)
{configBuilder.WithPropertyFilter((entityEntry, propertyEntry) entityEntry.Metadata.GetTableName() ! tableName propertyEntry.Metadata.GetColumnName() ! columnName);return configBuilder;
}
public static IAuditConfigBuilder WithPropertyFilter(this IAuditConfigBuilder configBuilder, FuncPropertyEntry, bool filterFunc)
{configBuilder.WithPropertyFilter((entity, prop) filterFunc.Invoke(prop));return configBuilder;
}
IAuditPropertyEnricher上面由提到有时候我们希望审计记录能够记录更多的信息需要提供给用户一些自定义的扩展点这里的 Enricher 的实现参考了 Serilog 里的做法我们可以自定义一个 IAuditPropertyEnricher 来丰富审计的信息默认提供了 AuditPropertyEnricher可以支持 key-value 形式的补充信息实现如下public class AuditPropertyEnricher : IAuditPropertyEnricher
{private readonly string _propertyName;private readonly FuncAuditEntry, object _propertyValueFactory;private readonly bool _overwrite;private readonly FuncAuditEntry, bool _auditPropertyPredict null;public AuditPropertyEnricher(string propertyName, object propertyValue, bool overwrite false): this(propertyName, (auditEntry) propertyValue, overwrite){}public AuditPropertyEnricher(string propertyName, FuncAuditEntry, object propertyValueFactory, bool overwrite false): this(propertyName, propertyValueFactory, null, overwrite){}public AuditPropertyEnricher(string propertyName,FuncAuditEntry, object propertyValueFactory,FuncAuditEntry, bool auditPropertyPredict,bool overwrite false){_propertyName propertyName;_propertyValueFactory propertyValueFactory;_auditPropertyPredict auditPropertyPredict;_overwrite overwrite;}public void Enrich(AuditEntry auditEntry){if (_auditPropertyPredict?.Invoke(auditEntry) ! false){auditEntry.WithProperty(_propertyName, _propertyValueFactory, _overwrite);}}
}
为了方便使用提供了一些方便的扩展方法public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, object value, bool overwrite false)
{configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, value, overwrite));return configBuilder;
}
public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, FuncAuditEntry valueFactory, bool overwrite false)
{configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, overwrite));return configBuilder;
}
public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, object value, FuncAuditEntry, bool predict, bool overwrite false)
{configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, e value, predict, overwrite));return configBuilder;
}
public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, FuncAuditEntry, object valueFactory, FuncAuditEntry, bool predict, bool overwrite false)
{configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, predict, overwrite));return configBuilder;
}
IAuditStore之前的测试都是基于数据库来的审计记录也是放在数据库里的有时候可能不希望和原始数据存在一个数据库里有时候甚至希望不放在数据库里为了实现可以自定义的存储提供了一个 IAuditStore 的接口提供给用户可以自定义审计信息存储的可能。public interface IAuditStore
{Task Save(ICollectionAuditEntry auditEntries);
}
使用DbContext 配置默认提供了一个 AuditDbContextBase 和 AuditDbContext他们的区别在于 AuditDbContext 会创建一张 AuditRecords 表记录审计信息 AuditDbContextBase 则不会只会写配置的存储。如果希望提供自动审计的功能新建 DbContext 的时候需要继承 AuditDbContext 或 AuditDbContextBase审计配置AuditConfig.Configure(builder
{builder// 配置操作用户获取方式.WithUserIdProvider(EnvironmentAuditUserIdProvider.Instance.Value)//.WithUnModifiedProperty() // 保存未修改的属性,默认只保存发生修改的属性// 保存更多属性.EnrichWithProperty(MachineName, Environment.MachineName).EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName)// 保存到自定义的存储.WithStoreAuditFileStore().WithStoreAuditFileStore(logs0.txt)// 忽略指定实体.IgnoreEntityAuditRecord()// 忽略指定实体的某个属性.IgnorePropertyTestEntity(t t.CreatedAt)// 忽略所有属性名称为 CreatedAt 的属性.IgnoreProperty(CreatedAt);
});
如果希望暂时禁用审计可以使用 AuditConfig.DisableAudit() 来禁用之后恢复可以使用 AuditConfig.EnableAudit()// disable audit
AuditConfig.DisableAudit();
// enable audit
// AuditConfig.EnableAudit();
More暂时想到的特性只有这些了想要更多特性欢迎 Issue PR项目地址https://github.com/WeihanLi/WeihanLi.EntityFrameworkReferencehttps://www.meziantou.net/entity-framework-core-history-audit-table.htmhttps://github.com/WeihanLi/WeihanLi.EntityFramework