EF Core 数据变更自动审计设计
EF Core 數(shù)據(jù)變更自動審計(jì)設(shè)計(jì)
Intro
有的時候我們需要知道每個數(shù)據(jù)表的變更記錄以便做一些數(shù)據(jù)審計(jì),數(shù)據(jù)恢復(fù)以及數(shù)據(jù)同步等之類的事情, EF 自帶了對象追蹤,使得我們可以很方便的做一些審計(jì)工作,每次變更發(fā)生了什么變化都變得很清晰,于是就基于 EF 封裝了一層數(shù)據(jù)變更自動審計(jì)
使用效果
測試代碼:
private static void AutoAuditTest() {// 審計(jì)配置AuditConfig.Configure(builder =>{builder// 配置操作用戶獲取方式.WithUserIdProvider(EnvironmentAuditUserIdProvider.Instance.Value)//.WithUnModifiedProperty() // 保存未修改的屬性,默認(rèn)只保存發(fā)生修改的屬性// 保存更多屬性.EnrichWithProperty("MachineName", Environment.MachineName).EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName)// 保存到自定義的存儲.WithStore<AuditFileStore>().WithStore<AuditFileStore>("logs.log")// 忽略指定實(shí)體.IgnoreEntity<AuditRecord>()// 忽略指定實(shí)體的某個屬性.IgnoreProperty<TestEntity>(t => t.CreatedAt)// 忽略所有屬性名稱為 CreatedAt 的屬性.IgnoreProperty("CreatedAt");});DependencyResolver.TryInvokeService<TestDbContext>(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.TryInvokeService<TestDbContext>(dbContext =>{dbContext.Remove(new TestEntity(){Id = 2});dbContext.SaveChanges();});// disable auditAuditConfig.DisableAudit(); }查看審計(jì)記錄信息:
可以看到,每次數(shù)據(jù)變更都會被記錄下來, CreatedAt 沒有記錄是因?yàn)樯厦媾渲玫暮雎?CreatedAt 屬性信息的記錄。
這里的 TableName ,屬性名稱和 Entity 定義的不同是為了測試列名和屬性名稱不一致的情況,實(shí)際記錄的是數(shù)據(jù)庫里的表名稱和列名稱,之所以這樣設(shè)計(jì)考慮的是可能多個應(yīng)用使用同一張表,但是不同的應(yīng)用里可能使用的 Entity 和 Property 都不同,所以統(tǒng)一使用了數(shù)據(jù)庫的表名稱和字段名稱。
OperationType是一個枚舉,1是新增,2是刪除,3是修改。
Extra 列對應(yīng)的就是我們自定義的增加的審計(jì)屬性
UpdatedBy 是我們配置的 UserIdProvider 所提供的操作用戶的信息
值得注意的是最后一條變更記錄,這條數(shù)據(jù)的刪除沒有經(jīng)過數(shù)據(jù)庫查詢,直接刪除的,EF 不知道原本的除了主鍵之外的信息,所以記錄的原始信息可能不準(zhǔn)確,不過還是知道誰刪除的這一條數(shù)據(jù),對比之前的變更還是可以滿足需求的。
實(shí)現(xiàn)原理
實(shí)現(xiàn)的原理是基于 EF 的內(nèi)置的 Change Tracking 來實(shí)現(xiàn)的,EF 每次 SaveChanges 之前都會檢測變更,每條變更的記錄都會記錄變更前的屬性值以及變更之后的屬性值,因此我們可以在 SaveChanges 之前記錄變更前后的屬性,對于數(shù)據(jù)庫生成的值,如 SQL Server 里的自增主鍵,在保存之前,屬性的會被標(biāo)記為 IsTemporary ,保存成功之后會自動更新,在保存之后可以獲取到數(shù)據(jù)庫生成的值。
實(shí)現(xiàn)代碼
首先實(shí)現(xiàn)一個 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 Task<int> SaveChangesAsync(CancellationToken cancellationToken = default){await BeforeSaveChanges();var result = await base.SaveChangesAsync(cancellationToken);await AfterSaveChanges();return result;}接著來實(shí)現(xiàn)一個用來自動審計(jì)的 AuditDbContextBase,核心代碼如下:
public abstract class AuditDbContextBase : DbContextBase {protected AuditDbContextBase(){}protected AuditDbContextBase(DbContextOptions dbContextOptions) : base(dbContextOptions){}protected List<AuditEntry> AuditEntries { get; set; }protected override Task BeforeSaveChanges(){AuditEntries = new List<AuditEntry>();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} }此時我們已經(jīng)可以實(shí)現(xiàn)自動的審計(jì)處理了,但是在實(shí)際業(yè)務(wù)處理的過程中,往往我們還會有更多的需求,
比如上面的實(shí)現(xiàn)還沒有加入更新人,不知道是由誰來操作的,有些字段可能不希望被記錄下來,或者有些表不要記錄,還有我們向增加一些自定義的屬性,比如多個應(yīng)用操作同一個數(shù)據(jù)庫表的時候我們可能希望記錄下來是哪一個用戶通過哪一個應(yīng)用來更新的等等,所以之前上面的實(shí)現(xiàn)還是不能夠?qū)嶋H應(yīng)用的,于是我又在上面的基礎(chǔ)上增加了一些配置以及擴(kuò)展,使得自動審計(jì)擴(kuò)展性更好,可定制性更強(qiáng)。
擴(kuò)展
UserIdProvider
我們可以通過 UserIdProvider 來實(shí)現(xiàn)操作用戶信息的獲取,默認(rèn)提供兩個實(shí)現(xiàn),定義如下:
public interface IAuditUserIdProvider {string GetUserId(); }默認(rèn)實(shí)現(xiàn):
// 獲取 Environment.UserName public class EnvironmentAuditUserIdProvider : IAuditUserIdProvider {private EnvironmentAuditUserIdProvider(){}public static Lazy<EnvironmentAuditUserIdProvider> Instance = new Lazy<EnvironmentAuditUserIdProvider>(() => new EnvironmentAuditUserIdProvider(), true);public string GetUserId() => Environment.UserName; } // 獲取 Thread.CurrentPrincipal.Identity.Name public class ThreadPrincipalUserIdProvider : IAuditUserIdProvider {public static Lazy<ThreadPrincipalUserIdProvider> Instance = new Lazy<ThreadPrincipalUserIdProvider>(() => new ThreadPrincipalUserIdProvider(), true);private ThreadPrincipalUserIdProvider(){}public string GetUserId() => Thread.CurrentPrincipal?.Identity?.Name; }當(dāng)然如果是 asp.net core 你也可以實(shí)現(xiàn)相應(yīng)的基于 HttpContext 實(shí)現(xiàn)的 UserIdProvider
Filters
基于我們可能希望忽略一些實(shí)體或?qū)傩杂涗?#xff0c;所以有必要增加 Filter 的記錄
基于實(shí)體的 Filter: Func<EntityEntry,bool>
基于屬性的 Filter: Func<EntityEntry,PropertyEntry,bool>
為了使用方便定義了一些擴(kuò)展方法:
public static IAuditConfigBuilder IgnoreEntity(this IAuditConfigBuilder configBuilder, Type entityType) {configBuilder.WithEntityFilter(entityEntry => entityEntry.Entity.GetType() != entityType);return configBuilder; } public static IAuditConfigBuilder IgnoreEntity<TEntity>(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, Func<EntityEntry, bool> filterFunc) {configBuilder.WithEntityFilter(filterFunc);return configBuilder; } public static IAuditConfigBuilder IgnoreProperty<TEntity>(this IAuditConfigBuilder configBuilder, Expression<Func<TEntity, 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, Func<PropertyEntry, bool> filterFunc) {configBuilder.WithPropertyFilter((entity, prop) => filterFunc.Invoke(prop));return configBuilder; }IAuditPropertyEnricher
上面由提到有時候我們希望審計(jì)記錄能夠記錄更多的信息,需要提供給用戶一些自定義的擴(kuò)展點(diǎn),這里的 Enricher 的實(shí)現(xiàn)參考了 Serilog 里的做法,我們可以自定義一個 IAuditPropertyEnricher ,來豐富審計(jì)的信息,默認(rèn)提供了 AuditPropertyEnricher,可以支持 key-value 形式的補(bǔ)充信息,實(shí)現(xiàn)如下:
public class AuditPropertyEnricher : IAuditPropertyEnricher {private readonly string _propertyName;private readonly Func<AuditEntry, object> _propertyValueFactory;private readonly bool _overwrite;private readonly Func<AuditEntry, bool> _auditPropertyPredict = null;public AuditPropertyEnricher(string propertyName, object propertyValue, bool overwrite = false): this(propertyName, (auditEntry) => propertyValue, overwrite){}public AuditPropertyEnricher(string propertyName, Func<AuditEntry, object> propertyValueFactory, bool overwrite = false): this(propertyName, propertyValueFactory, null, overwrite){}public AuditPropertyEnricher(string propertyName,Func<AuditEntry, object> propertyValueFactory,Func<AuditEntry, 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);}} }為了方便使用,提供了一些方便的擴(kuò)展方法:
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, Func<AuditEntry> valueFactory, bool overwrite = false) {configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, overwrite));return configBuilder; } public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, object value, Func<AuditEntry, 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, Func<AuditEntry, object> valueFactory, Func<AuditEntry, bool> predict, bool overwrite = false) {configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, predict, overwrite));return configBuilder; }IAuditStore
之前的測試都是基于數(shù)據(jù)庫來的,審計(jì)記錄也是放在數(shù)據(jù)庫里的,有時候可能不希望和原始數(shù)據(jù)存在一個數(shù)據(jù)庫里,有時候甚至希望不放在數(shù)據(jù)庫里,為了實(shí)現(xiàn)可以自定義的存儲,提供了一個 IAuditStore 的接口,提供給用戶可以自定義審計(jì)信息存儲的可能。
public interface IAuditStore {Task Save(ICollection<AuditEntry> auditEntries); }使用
DbContext?配置
默認(rèn)提供了一個 AuditDbContextBase 和 AuditDbContext,他們的區(qū)別在于 AuditDbContext 會創(chuàng)建一張 AuditRecords 表,記錄審計(jì)信息, AuditDbContextBase 則不會,只會寫配置的存儲。
如果希望提供自動審計(jì)的功能,新建 DbContext 的時候需要繼承 AuditDbContext 或 AuditDbContextBase
審計(jì)配置
AuditConfig.Configure(builder => {builder// 配置操作用戶獲取方式.WithUserIdProvider(EnvironmentAuditUserIdProvider.Instance.Value)//.WithUnModifiedProperty() // 保存未修改的屬性,默認(rèn)只保存發(fā)生修改的屬性// 保存更多屬性.EnrichWithProperty("MachineName", Environment.MachineName).EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName)// 保存到自定義的存儲.WithStore<AuditFileStore>().WithStore<AuditFileStore>("logs0.txt")// 忽略指定實(shí)體.IgnoreEntity<AuditRecord>()// 忽略指定實(shí)體的某個屬性.IgnoreProperty<TestEntity>(t => t.CreatedAt)// 忽略所有屬性名稱為 CreatedAt 的屬性.IgnoreProperty("CreatedAt"); });如果希望暫時禁用審計(jì)可以使用 AuditConfig.DisableAudit() 來禁用,之后恢復(fù)可以使用 AuditConfig.EnableAudit()
// disable audit AuditConfig.DisableAudit(); // enable audit // AuditConfig.EnableAudit();More
暫時想到的特性只有這些了,想要更多特性?歡迎 Issue & PR
項(xiàng)目地址:https://github.com/WeihanLi/WeihanLi.EntityFramework
Reference
https://www.meziantou.net/entity-framework-core-history-audit-table.htm
https://github.com/WeihanLi/WeihanLi.EntityFramework
總結(jié)
以上是生活随笔為你收集整理的EF Core 数据变更自动审计设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ASP.NET Core分布式项目实战(
- 下一篇: 使用Docker-Compose搭建高可