[ASPNETCORE] 抛砖引玉,EFCORE 软删除和自动添加审计的实现
很久没更新博客了,主要是换了新的环境,从netcore
转回了framework
,突然没有学习和探究的欲望了。
以前项目首选Dapper
来操作数据库,反正就是 SQL 一把梭,很爽很暴力。但新单位要求使用 EntityFramework6,无语凝噎中,写一篇关于EFCore 的文章,算是我最后的倔强吧。
EF 用的真的不多,所以这里就只能聊聊两点,一个是批量注册全局软删除筛选器,以及由此引出的自动添加审计功能的实现。
批量添加软删除全局筛选器
方法一,这是google出来的一个老外的写法,感觉比我常用的(方法二)简洁
第一步:新建文件,内容如下:
public static class SoftDeleteQueryExtension
{
public static void AddSoftDeleteQueryFilter(this IMutableEntityType entityData)
{
var methodToCall = typeof(SoftDeleteQueryExtension)
.GetMethod(nameof(GetSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
?.MakeGenericMethod(entityData.ClrType);
if (methodToCall is { })
{
var filter = methodToCall.Invoke(null, new object[] { });
entityData.SetQueryFilter((LambdaExpression)filter);
}
entityData.AddIndex(entityData.FindProperty(nameof(ISoftDeleted.IsDeleted)));
}
private static LambdaExpression GetSoftDeleteFilter<TEntity>()
where TEntity : class, ISoftDeleted
{
Expression<Func<TEntity, bool>> filter = x => !x.IsDeleted;
return filter;
}
}
第二步:重写DbContext
的OnModelCreating(ModelBuilder builder)
方法
protected override void OnModelCreating(ModelBuilder builder)
{
foreach (var mutableEntityType in builder.Model.GetEntityTypes())
{
if (typeof(ISoftDeleted).IsAssignableFrom(mutableEntityType.ClrType))
{
mutableEntityType.AddSoftDeleteQueryFilter();
}
}
}
方法二
第一步:在你的 DbContext
文件中增加如下方法:
private static void RegisterGlobalFilter<T>(ModelBuilder builder) where T : class
{
// ExpressionUtil 后面补充内容的表达式目录树部分有完整代码
var expr = ExpressionUtil.True<T>();
if (typeof(ISoftDeleted).IsAssignableFrom(typeof(T)))
{
expr = expr.And(t => ((ISoftDeleted)t).IsDel == false);
}
if (typeof(ITenantRequired).IsAssignableFrom(typeof(T)))
{
expr = expr.And(t => ((ITenantRequired)t).TenantId == _provider.GetTenantId());
}
builder.Entity<T>().HasQueryFilter(expr);
}
第二步:重写DbContext
的OnModelCreating(ModelBuilder builder)
方法
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var method = typeof(你的DbContext文件).GetMethod("RegisterGlobalFilter",
BindingFlags.Static | BindingFlags.NonPublic);
if (method != null)
{
var types = modelBuilder.Model.GetEntityTypes().Select(t => t.ClrType).ToList();
foreach (var type in types)
{
method.MakeGenericMethod(type).Invoke(this, new object[] { modelBuilder });
}
}
}
说起来,第一种的用法我是第一次见到,主要是IMutableEntityType
没用过也不了解,所以个人感觉有点厉害,第二个相对更好理解一些,其实就是下面这句的封装:
modelBuilder.Entity<Person>().HasQueryFilter(x => x.IsDel == false);
好了,添加全局筛选器的内容就结束了,很简单,抄来用即可。
其实同样方式是可以用来添加租户的全局筛选器的,问题是租户从哪里来?
不废话,直接上代码了:
public class TestDbContext : DbContext
{
private readonly IAuditProvider _provider;
public TestDbContext(DbContextOptions<BodyTemperatureDbContext> options, IAuditProvider provider) : base(options)
{
_provider = provider;
}
// ... 其他代码
}
可以看到,构造函数中多了一个IAuditProvider
,这个接口很简单,就是返回一系列跟当前用户有关的内容。
public interface IAuditProvider
{
/// <summary>
/// 获取用户名
/// </summary>
/// <returns></returns>
string GetName();
/// <summary>
/// 获取用户ID
/// </summary>
/// <returns></returns>
int GetId();
/// <summary>
/// 获取租户ID
/// </summary>
/// <returns></returns>
int GetTenantId();
/// <summary>
/// 获取部门ID
/// </summary>
/// <returns></returns>
int GetDepartmentId();
}
先不用管这个接口如何实现,反正有了它,并将其作为依赖项引入到DbContext中,就可以引用了。
在原方法二的基础上给RegisterGlobalFilter
方法加点代码:
private static void RegisterGlobalFilter<T>(ModelBuilder builder) where T : class
{
var expr = ExpressionUtil.True<T>();
if (typeof(ISoftDeleted).IsAssignableFrom(typeof(T)))
{
expr = expr.And(t => ((ISoftDeleted)t).IsDel == false);
}
// 手动高亮
if (typeof(ITenantRequired).IsAssignableFrom(typeof(T)))
{
expr = expr.And(t => ((ITenantRequired)t).TenantId == _provider.GetTenantId());
}
builder.Entity<T>().HasQueryFilter(expr);
}
这样就搞定了... 过于简单,有水字数的嫌疑...
发散一下思维
有了这个IAuditProvider
,不但可以添加全局筛选条件,还可以实现自动创建、修改、删除审计,反正好处一堆,但需要重写SaveChange方法,有代码洁癖者慎用
个人对ef用的并不熟练,下面的代码主要目标是实现功能,实现上并没有经过太多考量,算抛砖引玉吧
public override int SaveChanges()
{
SetAudit();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
SetAudit();
return base.SaveChangesAsync(cancellationToken);
}
private void SetAudit()
{
var userId = _provider.GetId();
var userName = _provider.GetName();
var tenantId = _provider.GetTenantId();
// 新增的时候添加自动添加创建操作的审计信息,
// 个人习惯同时操作更新的审计,毕竟第一次,既是添加也是最后一次修改,没毛病
var createdEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added).Select(e => e.Entity);
foreach (var entity in createdEntities)
{
if (entity is ICreateAudit createAudit)
{
createAudit.Creator = userName;
createAudit.CreateAt = DateTime.Now;
createAudit.CreateBy = userId;
}
if (entity is IEditAudit updateAudit)
{
updateAudit.Updater = userName;
updateAudit.UpdateAt = DateTime.Now;
updateAudit.UpdateBy = userId;
}
if (entity is ITenantRequired tenantEntity)
{
if (tenantEntity.TenantId == 0)
{
tenantEntity.TenantId = tenantId;
}
}
}
// 如果是修改,要把创建审计、软删除、租户等相关字段忽略掉;
// 同时要更新 最后一次修改的审计信息
var modifiedEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified).Select(e => e.Entity);
foreach (var entity in modifiedEntities)
{
if (entity is IEditAudit audit)
{
audit.UpdateBy = userId;
audit.Updater = userName;
audit.UpdateAt = DateTime.Now;
}
if (entity is ICreateAudit createAudit)
{
Entry(createAudit).Property(e => e.CreateBy).IsModified = false;
Entry(createAudit).Property(e => e.Creator).IsModified = false;
Entry(createAudit).Property(e => e.CreateAt).IsModified = false;
}
if (entity is ISoftDeleted deletedAudit)
{
Entry(deletedAudit).Property(e => e.IsDel).IsModified = false;
}
if (entity is ITenantRequired tenantEntity)
{
Entry(tenantEntity).Property(e => e.TenantId).IsModified = false;
}
}
// 删除操作,如果是软删除的实体,要把操作改成update,并且只更新IsDeleted字段为true即可
// 我这里没有删除审计的功能,如果需要,自己添加即可
var deletedEntities = ChangeTracker.Entries().Where(e => e.State == EntityState.Deleted);
foreach (var entry in deletedEntities)
{
var entity = entry.Entity;
if (entity is ISoftDeleted softDeleted)
{
softDeleted.IsDel = true;
entry.State = EntityState.Unchanged;
entry.Property("IsDel").IsModified = true;
}
}
}
上面的代码粘贴上来后,手动改动了一点,如果跑起来有问题,请留言提醒,还有就是上面用到了一些自定义的类,都补充在下面了
一. 表达式目录树的扩展,就是上面用到的ExpressionUtil
以及表达式目录树And
和Or
拼接方法的封装
下面的代码有的是网上抄的,有的自己写的,至少应该是可以用的
先创建这个工具类 ParameterRebind
public class ParameterRebind : ExpressionVisitor
{
/// <summary>
/// 参数表达式的映射,前面是原始参数表达式,后面是要替换的参数表达式
/// </summary>
readonly Dictionary<ParameterExpression, ParameterExpression> _map;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="map">The map.</param>
private ParameterRebind(Dictionary<ParameterExpression, ParameterExpression> map)
{
_map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
/// <summary>
/// 访问参数
/// </summary>
/// <param name="p">The p.</param>
/// <returns>Expression</returns>
protected override Expression VisitParameter(ParameterExpression p)
{
if (_map.TryGetValue(p, out var replacement))
{
p = replacement;
}
return base.VisitParameter(p);
}
/// <summary>
/// 静态方法,替换参数
/// </summary>
/// <param name="map">参数映射</param>
/// <param name="exp">要处理的表达式</param>
/// <returns>Expression</returns>
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebind(map).Visit(exp);
}
}
接着创建 ExpressionUtil
public static class ExpressionUtil
{
// 返回一个恒定为true的表达式,相当于 where 1=1,一般要拼接表达式,都是从这一句开始
public static Expression<Func<T, bool>> True<T>()
{
return obj => true;
}
// 这个从来没用过,hahaha...
public static Expression<Func<T, bool>> False<T>()
{
return obj => false;
}
/// <summary>
/// 组合And
/// </summary>
/// <returns></returns>
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.Compose(second, Expression.AndAlso);
}
/// <summary>
/// 组合Or
/// </summary>
/// <returns></returns>
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.Compose(second, Expression.OrElse);
}
private static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
{
var map = first.Parameters
.Select((f, i) => new { f, s = second.Parameters[i] })
.ToDictionary(p => p.s, p => p.f);
var secondBody = ParameterRebind.ReplaceParameters(map, second.Body);
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
}
}
二. 那个IAuditProvider 到底怎么用?
定义一个实现,从登录用户中获取,类似 HzcClaimTypes 就是自己定义的字符串,主要是嫌弃官方提供的太长了。
这个实现比较简单,有些库里面是尝试从多个渠道获取用户数据:
- 登录用户获取(通过cookie,token,session等)
- 请求头
- 请求参数
我个人比较喜欢微软封装的认证和鉴权的做法,所以所有项目无论如何都会把用户信息附加到HttpContext的User对象上,所以这个 DefaultAuditProvider 足够用了。
public class DefaultAuditProvider : IAuditProvider
{
private readonly IHttpContextAccessor _accessor;
public AuditProvider(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public string GetName()
{
return _accessor.HttpContext.User?.Identity.Name ?? "";
}
public int GetId()
{
return _accessor.HttpContext.User.GetInt(HzcClaimTypes.Id);
}
public int GetTenantId()
{
return _accessor.HttpContext.User.GetInt(HzcClaimTypes.TenantId);
}
public int GetDepartmentId()
{
return 0;
}
}
在 Startup 的 ConfigureServices 方法中添加:
services.AddSingleton<IAuditProvider, DefaultAuditProvider>();
搞定收工...