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 audit
AuditConfig.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,重写 SaveChangesSaveChangesAsync 方法,增加

BeforeSaveChangesAfterSaveChanges 方法,用于处理我们要自定义的保存之前和保存之后的逻辑。

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 TemporaryProperties
if (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 null
auditEntry.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 配置

默认提供了一个 AuditDbContextBaseAuditDbContext,他们的区别在于 AuditDbContext 会创建一张 AuditRecords 表,记录审计信息,AuditDbContextBase 则不会,只会写配置的存储。

如果希望提供自动审计的功能,新建 DbContext 的时候需要继承 AuditDbContextAuditDbContextBase

审计配置

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

EF Core 数据变更自动审计设计的更多相关文章

  1. [EF Core]数据迁移(二)

    摘要 在实际项目中,大多都需要对业务逻辑以及操作数据库的逻辑进行分成操作,这个时候该如何进行数据的迁移呢? 步骤 上篇文章:EF Core数据迁移操作 比如,我们将数据上下文放在了Data层. 看一下 ...

  2. ef core数据迁移的一点小感悟

    ef core在针对mysql数据迁移的时候,有些时候没法迁移...有两种情况没法迁移,一种是因为efcore的bug问题导致没法迁移,这个在github上有个问题集,另外一种是对数据表进行较大幅度的 ...

  3. EF Core CodeFirst数据库自动迁移

    开发过程中都会遇到数据库数据结构更新的问题,怎么对数据库更新进行版本控制呢? 不同的项目对数据库版本更新控制的方式不同,常用的有第三方Evolve,开发人员将数据库更新脚本按照版本号的放在一起,然后执 ...

  4. EF Core 数据过滤

    1 前言 本文致力于将一种动态数据过滤的方案描述出来(基于 EF Core 官方的数据筛选器),实现自动注册,多个条件过滤,单条件禁用(实际上是参考ABP的源码),并尽量让代码保持 EF Core 的 ...

  5. EF Core数据访问入门

    重要概念 Entity Framework (EF) Core 是轻量化.可扩展.开源和跨平台的数据访问技术,它还是一 种对象关系映射器 (ORM),它使 .NET 开发人员能够使用面向对象的思想处理 ...

  6. EF Core数据迁移操作

    摘要 在开发中,使用EF code first方式开发,那么如果涉及到数据表的变更,该如何做呢?当然如果是新项目,删除数据库,然后重新生成就行了,那么如果是线上的项目,数据库中已经有数据了,那么删除数 ...

  7. Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作

    前言 前段时间写了EF core实现多租户的文章,实现了根据数据库,数据表进行多租户数据隔离. 今天开始写按照Schema分离的文章. 其实还有一种,是通过在数据表内添加一个字段做多租户的,但是这种模 ...

  8. .NET 云原生架构师训练营(模块二 基础巩固 EF Core 更新和迁移)--学习笔记

    2.4.6 EF Core -- 更新 状态 自动变更检测 不查询删除和更新 并发 状态 Entity State Property State Entity State Added 添加 Uncha ...

  9. WithOne 实体关系引起 EF Core 自动删除数据

    最近遇到了一个 EF Core 的恐怖问题,在添加数据时竟然会自动删除数据库中已存在的数据,经过追查发现是一个多余的实体关系配置引起的. modelBuilder.Entity<Question ...

随机推荐

  1. 深度学习遥感影像(哨兵2A/B)超分辨率

    这段时间,用到了哨兵影像,遇到了一个问题,就是哨兵影像,它的RGB/NIR波段是10米分辨率的,但是其他波段是20米和60米的,这就需要pansharpening了,所以我们需要设计一种算法来进行解决 ...

  2. 彻底理解使用JavaScript 将Json数据导出CSV文件

    前言 将数据报表导出,是web数据报告展示常用的附带功能.通常这种功能都是用后端开发人员编写的.今天我们主要讲的是直接通过前端js将数据导出Excel的CSV格式的文件. 原理 首先在本地用Excel ...

  3. JZOJ 5246. 【NOIP2017模拟8.8A组】Trip(trip)

    5246. [NOIP2017模拟8.8A组]Trip(trip) (File IO): input:trip.in output:trip.out Time Limits: 1500 ms Memo ...

  4. 初窥构建之法——记2020BUAA软工个人博客作业

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 完成一次完整的软件开发经历并以博客的方式记录开发过程的心得掌握 ...

  5. 一起了解 .Net Foundation 项目 No.14

    .Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. .NET Core .NE ...

  6. 【猫狗数据集】谷歌colab之使用pytorch读取自己数据集(猫狗数据集)

    之前在:https://www.cnblogs.com/xiximayou/p/12398285.html创建好了数据集,将它上传到谷歌colab 在colab上的目录如下: 在utils中的rdat ...

  7. SpringBoot1.5.10.RELEASE配置mybatis的逆向工程

    在application.properties配置扫描等,不做多说 1.在pom配置文件中引入mybatis和mysql的依赖,如下: <dependency> <groupId&g ...

  8. javascript中this指向的问题

    javascript中this只有函数执行时候才能确定到底指向谁,实际this最终指向是那个调用它的对象. 1,匿名函数中的this——window function foo(){ var lastN ...

  9. iOS8 定位失败问题

    iOS7升级到iOS8后,百度地图 iOS SDK 中的定位功能不可用,给广大开发者带来了不便,在此向大家分享一个方法来解决次问题.(官方的适配工作还在进行中,不久将会和广大开发者见面) 1.在inf ...

  10. 编译 openwrt 及初始配置

    主机为 ubuntu 14 x64 硬件: 优酷土豆宝 cpuMT7620A,内存128M,flash 32M有2个源,用哪个也可以git clone https://github.com/openw ...