日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

EF Core 数据变更自动审计设计

發布時間:2023/12/4 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 EF Core 数据变更自动审计设计 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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)// 保存到自定義的存儲.WithStore<AuditFileStore>().WithStore<AuditFileStore>("logs.log")// 忽略指定實體.IgnoreEntity<AuditRecord>()// 忽略指定實體的某個屬性.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(); }

查看審計記錄信息:

可以看到,每次數據變更都會被記錄下來, 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 Task<int> 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 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} }

此時我們已經可以實現自動的審計處理了,但是在實際業務處理的過程中,往往我們還會有更多的需求,

比如上面的實現還沒有加入更新人,不知道是由誰來操作的,有些字段可能不希望被記錄下來,或者有些表不要記錄,還有我們向增加一些自定義的屬性,比如多個應用操作同一個數據庫表的時候我們可能希望記錄下來是哪一個用戶通過哪一個應用來更新的等等,所以之前上面的實現還是不能夠實際應用的,于是我又在上面的基礎上增加了一些配置以及擴展,使得自動審計擴展性更好,可定制性更強。

擴展

UserIdProvider

我們可以通過 UserIdProvider 來實現操作用戶信息的獲取,默認提供兩個實現,定義如下:

public interface IAuditUserIdProvider {string GetUserId(); }

默認實現:

// 獲取 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; }

當然如果是 asp.net core 你也可以實現相應的基于 HttpContext 實現的 UserIdProvider

Filters

基于我們可能希望忽略一些實體或屬性記錄,所以有必要增加 Filter 的記錄

基于實體的 Filter: Func<EntityEntry,bool>

基于屬性的 Filter: Func<EntityEntry,PropertyEntry,bool>

為了使用方便定義了一些擴展方法:

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

上面由提到有時候我們希望審計記錄能夠記錄更多的信息,需要提供給用戶一些自定義的擴展點,這里的 Enricher 的實現參考了 Serilog 里的做法,我們可以自定義一個 IAuditPropertyEnricher ,來豐富審計的信息,默認提供了 AuditPropertyEnricher,可以支持 key-value 形式的補充信息,實現如下:

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

為了方便使用,提供了一些方便的擴展方法:

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

之前的測試都是基于數據庫來的,審計記錄也是放在數據庫里的,有時候可能不希望和原始數據存在一個數據庫里,有時候甚至希望不放在數據庫里,為了實現可以自定義的存儲,提供了一個 IAuditStore 的接口,提供給用戶可以自定義審計信息存儲的可能。

public interface IAuditStore {Task Save(ICollection<AuditEntry> 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)// 保存到自定義的存儲.WithStore<AuditFileStore>().WithStore<AuditFileStore>("logs0.txt")// 忽略指定實體.IgnoreEntity<AuditRecord>()// 忽略指定實體的某個屬性.IgnoreProperty<TestEntity>(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.EntityFramework

Reference

  • https://www.meziantou.net/entity-framework-core-history-audit-table.htm

  • https://github.com/WeihanLi/WeihanLi.EntityFramework

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的EF Core 数据变更自动审计设计的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。