zoukankan      html  css  js  c++  java
  • [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;
        }
    }
    

    第二步:重写DbContextOnModelCreating(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);
    }
    

    第二步:重写DbContextOnModelCreating(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以及表达式目录树AndOr 拼接方法的封装

    下面的代码有的是网上抄的,有的自己写的,至少应该是可以用的

    先创建这个工具类 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 就是自己定义的字符串,主要是嫌弃官方提供的太长了。

    这个实现比较简单,有些库里面是尝试从多个渠道获取用户数据:

    1. 登录用户获取(通过cookie,token,session等)
    2. 请求头
    3. 请求参数

    我个人比较喜欢微软封装的认证和鉴权的做法,所以所有项目无论如何都会把用户信息附加到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>();
    

    搞定收工...

  • 相关阅读:
    线程与并发系列一:Lock、Monitor、UserSpinLock
    什么是WebService
    异步和多线程有什么区别
    java.sql.SQLException: The server time zone value '' is unrecognized or represents
    java.sql.SQLException: Unable to load authentication plugin 'caching_sha2_password'.
    本地如何查看zookeeper注册了哪些服务
    maven的archetype
    Windows下安装ZooKeeper
    Dubbo架构和原理
    IntelliJ IDEA 2019.2.4破解
  • 原文地址:https://www.cnblogs.com/diwu0510/p/14497548.html
Copyright © 2011-2022 走看看