『贴代码』
我的博文
个人作品
优选问答
「搜索」
【本期话题】更多
三人寄语更多
你连F12都不关注,你好意思说你是前端?
点赞:0
抛开需求讲架构,和纸上谈兵无差!
点赞:1
测试没问题的不一定没问题,测试有问题的那肯定有问题!
点赞:2
往往会为了一个项目,搭建适合他专属的脚手架!
点赞:1
时间与空间总是在换来换去,鱼和熊掌往往不可同得!
点赞:0
实际遇到的问题往往在那些视频课程中是不会出现的!
点赞:0
能通过内网IP访问的,尽量不要使用域名访问!
点赞:1
微信的app这个东西很鬼,有时候你刷新页面,会造成部分数据重置,部分不重置,不妨试试把对象放app.globalData里面去,会有意外惊喜!
点赞:0
慎用redis的同步我的意见是redis都走异步!!!
点赞:0
没有最好的语言,只有更合适的语言!
点赞:0
PasteForm(ABP)框架之实现更加灵活的类似多租户的归属过滤功能,比如只能查看自己的相关数据
尘埃 2025-05-31 52 6 0
在开发中,可能会遇到类似的需求,比如你只能查看你部门的数据,或者是只能查看你自己的数据,如果你的项目有非常多的表,那要如何实现更加简单呢?

需求说明

在开发中,我们常会遇到一个问题,就是归属查询问题,比如只能查看我自己的,往往这个时候还附带了一个规则,比如有人是在这个规则之外的!
1.只能查看创建者自己创建的资料
2.只能查看我店铺的相关内容,不能查看别人店铺的
3.只能查看我部门的相关信息等

可能你会说,直接查询的时候加条件即可!道理就是这么个道理,哪怕是多租户其实也是这样实现的!

为啥不直接用多租户呢

多租户在我的项目中,用得非常非常少,更多的用的是灵活的协作模式,比如多店铺模式,多商户号模式等!

1.目前的多租户限制比较多,如果引入了,变成店铺+多租户,多了一层关系,别和我说扩展,我感觉扩展还是不如重写来得直接!
2.很多查询,其实并不是严格的多租户模式,比如有些组织,或者个人拥有多个店铺太正常了
3.范围限定,你不能多租户套多租户吧,但是实际需求中往往很多限定的,比如只能查看他自己创建的东西,外面又套了一层店铺

案例分析

在PasteDocument项目中,有这么一个需求,我只能查看我组织的信息,为啥我没用多租户做呢,当时也是考虑使用多租户的,后面放弃了,我宁愿每个表都加一个字段CompanyId

1.Company本身,我有很多自定义的规则
2.多租户用的是Guid,实际中给我的感觉数据库如果没有必要还是不要使用Guid作为主键的好,其实int/long完全够用,当然Guid也有他的优点所在,比如别人猜不到你的日增长,至于说基于id猜数据的,我只能说是你的权限有问题,而不是数据本身!
3.不够灵活,因为我一个用户率属于多个组织太正常了
4.后续扩展考虑

综上,你看需求不就来了么,好了,直接上代码!

解决思路

居然有多租户这个模式,那直接按照他的思路来改即可,对吧!

解读当前方案

下载ABP的源码后,我们在AbpDbContext中看到如下代码,主要是针对多租户的

    protected virtual Expression<Func<TEntity, bool>>? CreateFilterExpression<TEntity>()
        where TEntity : class
    {
        Expression<Func<TEntity, bool>>? expression = null;

        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");
        }

        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;
            expression = expression == null ? multiTenantFilter : QueryFilterExpressionHelper.CombineExpressions(expression, multiTenantFilter);
        }

        return expression;
    }

注意看上面的2个被引用的参数
IsMultiTenantFilterEnabled:是否需要过滤多租户
CurrentTenantId:当前的多租户是多少
然后上面的函数意思是基于2个变量生成一个表达式!
看看谁调用他了

    protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
        where TEntity : class
    {
        if (mutableEntityType.BaseType == null && ShouldFilterEntity<TEntity>(mutableEntityType))
        {
            var filterExpression = CreateFilterExpression<TEntity>();
            if (filterExpression != null)
            {
                var abc = modelBuilder.Entity<TEntity>();

                modelBuilder.Entity<TEntity>().HasAbpQueryFilter(filterExpression);
            }
        }
    }

感觉可以略过,意思就是判断Entity是否继承于设定的IMultiTenant

    protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
    {
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        return false;
    }

我们再看看ConfigureGlobalFilters被谁调用了

    protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
        where TEntity : class
    {
        if (mutableEntityType.IsOwned())
        {
            return;
        }

        if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity)))
        {
            return;
        }

        modelBuilder.Entity<TEntity>().ConfigureByConvention();

        ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
    }

这个判断,啥意思?防止自己调用自己?那估计是有递归查之类的,别管,继续上一级

    private static readonly MethodInfo ConfigureBasePropertiesMethodInfo
        = typeof(AbpDbContext<TDbContext>)
            .GetMethod(
                nameof(ConfigureBaseProperties),
                BindingFlags.Instance | BindingFlags.NonPublic
            )!;

这里用了一个,,,,
反射吧!
typeof xxx GetMethod
好家伙,直接避免直接接触啊!
看看谁调用他了

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        TrySetDatabaseProvider(modelBuilder);

        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            ConfigureBasePropertiesMethodInfo
                .MakeGenericMethod(entityType.ClrType)
                .Invoke(this, new object[] { modelBuilder, entityType });

            ConfigureValueConverterMethodInfo
                .MakeGenericMethod(entityType.ClrType)
                .Invoke(this, new object[] { modelBuilder, entityType });

            ConfigureValueGeneratedMethodInfo
                .MakeGenericMethod(entityType.ClrType)
                .Invoke(this, new object[] { modelBuilder, entityType });
        }
    }

看到上面的,有感觉没有?

protected override void OnModelCreating(ModelBuilder modelBuilder)

思路解读

看上面代码,意思就是在DbContext的时候,创建对应的过滤规则,这里有一个关键点!!!

误区一

就是上面说的2个变量IsMultiTenantFilterEnabled和CurrentTenantId
然后是表达式
Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, “TenantId”) == CurrentTenantId;
翻译过来,大致是这样的

where IsMultiTenantFilterEnabled==false || TenantId ='xxxxxxxxxx'

也就是说,无论IsMultiTenantFilterEnabled是true/false,这个表达式都是存在的

我之前的理解应该是,有启用的时候才拼接这个表达式,没有则没有这个表达式!

这个是理解误区之一!!

误区二

其实这个不是我的误区,是那几个AI的,为啥呢?
我问了AI这个功能如何实现,直接给我全部往OnModelCreating中写
然后我问了一句,不对劲啊,On Model Creating的字面意思就是模块创建的时候,难不成一直创建???
然后AI就进入死循环无法自拔了!!!

这里有一个重要点
1.DbContext是瞬时的,也就是每次都New一个
2.OnModelCreating只有第一次会执行,不是每次new DbContext都会执行!
3.也就是关键的2个变量IsMultiTenantFilterEnabled/CurrentTenantId其实是和DbContext一样的,瞬时的,也就是当前!!!

总结

结合上面的知识和误区一,误区二

我们可以把当前需求注入到DbContext中,你看官方AbpDbContext也是注入了

    protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;

    protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;

    protected virtual bool IsSoftDeleteFilterEnabled => DataFilter?.IsEnabled<ISoftDelete>() ?? false;

    public ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();

那我就把ICurrentUser注入,然后其他的按照多租户的流程走一遍即可!

实现代码

以下用我的测试项目来做例子,我做一个过滤UserId的

IUserIdEntity

这个呢直接参考IMultiTenant

    /// <summary>
    /// 
    /// </summary>
    public interface IUserIdEntity
    {
        /// <summary>
        /// 
        /// </summary>
        int UserId { get; set; }
    }

XXDbContext

然后改造我的SqliteDbContext

using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using PasteTest.usermodels;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Users;

namespace PasteTest
{
    /// <summary>
    /// 
    /// </summary>
    [ConnectionStringName(PasteTestDbProperties.SqliteConnectionStringName)]
    public class SqliteDbContext : AbpDbContext<SqliteDbContext>, IPasteTestDbContext
    {
        /* Add DbSet for each Aggregate Root here. Example:
         * public DbSet<Question> Questions { get; set; }
         */

        private readonly ICurrentUser _currentUser;

        private bool LimitUserId { get; set; } = false;

        private int CurrentUserId { get; set; } = 0;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="options"></param>
        /// <param name="currentUser"></param>
        public SqliteDbContext(DbContextOptions<SqliteDbContext> options, ICurrentUser currentUser)
            : base(options)
        {
            _currentUser = currentUser;
            if (_currentUser != null)
            {
                //这里是核心,主要判断当前的开关,当前登录用户的
                var limit = _currentUser.FindClaim("LimitUser")?.Value.To<bool>();
                if (limit.HasValue && limit.Value)
                {
                    var userid = _currentUser.FindClaim("UserId")?.Value.To<int>();
                    LimitUserId = true;
                    CurrentUserId = userid.Value;
                }
            }

        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="builder"></param>
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.ConfigurePasteTest();

            foreach (var entityType in builder.Model.GetEntityTypes())
            {
                if (typeof(IUserIdEntity).IsAssignableFrom(entityType.ClrType))
                {
                    MyPropertiesMethodInfo.MakeGenericMethod(entityType.ClrType).Invoke(this, new object[] { builder, entityType });
                }
            }
        }

        #region 以下作用为了在查询的时候添加过滤,和当前用户有关

        //注意SqliteDbContext要进行实际替换

        private static MethodInfo MyPropertiesMethodInfo = typeof(SqliteDbContext).GetMethod(nameof(ConfigureMyProperties), BindingFlags.Instance | BindingFlags.NonPublic);

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="modelBuilder"></param>
        /// <param name="mutableEntityType"></param>
        protected void ConfigureMyProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) where TEntity : class
        {
            if (mutableEntityType.IsOwned())
            {
                return;
            }
            if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity)))
            {
                return;
            }
            MyConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="modelBuilder"></param>
        /// <param name="mutableEntityType"></param>
        protected void MyConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) where TEntity : class
        {
            if (mutableEntityType.BaseType == null && typeof(IUserIdEntity).IsAssignableFrom(typeof(TEntity)))
            {
                var filterExpression = MyCreateFilterExpression<TEntity>();
                if (filterExpression != null)
                {
                    modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
                }
            }
        }

        /// <summary>
        /// 判断是否要创建过滤表达式
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        protected Expression<Func<TEntity, bool>> MyCreateFilterExpression<TEntity>()
        {
            Expression<Func<TEntity, bool>>? expression = null;// expression = base.CreateFilterExpression<TEntity>();
            if (typeof(IUserIdEntity).IsAssignableFrom(typeof(TEntity)))
            {
                //下面内容注意按照实际情况替换
                Expression<Func<TEntity, bool>> multiTenantFilter = e => !LimitUserId || EF.Property<int>(e, "UserId") == CurrentUserId;
                expression = expression == null ? multiTenantFilter : NeedCombineExpressions(expression, multiTenantFilter);
            }
            //多个在这里追加
            return expression;
        }

        /// <summary>
        /// 表达式拼接
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression1"></param>
        /// <param name="expression2"></param>
        /// <returns></returns>
        private static Expression<Func<T, bool>> NeedCombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            var parameter = Expression.Parameter(typeof(T));

            var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);
            var left = leftVisitor.Visit(expression1.Body);

            var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
            var right = rightVisitor.Visit(expression2.Body);

            return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left!, right!), parameter);
        }

        /// <summary>
        /// 
        /// </summary>
        private class ReplaceExpressionVisitor : ExpressionVisitor
        {
            private readonly Expression _oldValue;
            private readonly Expression _newValue;

            public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
            {
                _oldValue = oldValue;
                _newValue = newValue;
            }

            public override Expression? Visit(Expression? node)
            {
                return node == _oldValue ? _newValue : base.Visit(node);
            }
        }

        #endregion

        /// <summary>
        /// 用户信息
        /// </summary>
        public DbSet<UserInfo> UserInfo { get; set; }
        /// <summary>
        /// 权限信息
        /// </summary>
        public DbSet<RoleInfo> RoleInfo { get; set; }
        /// <summary>
        /// 角色信息
        /// </summary>
        public DbSet<GradeInfo> GradeInfo { get; set; }
        /// <summary>
        /// 角色权限
        /// </summary>
        public DbSet<GradeRole> GradeRole { get; set; }




        /// <summary>
        /// 用户绑定角色 解绑的直接删除数据
        /// </summary>
        public DbSet<UserGrade> UserGrade { get; set; }


        /// <summary>
        /// 测试表
        /// </summary>
        public DbSet<MyLinkTable> MyLinkTable { get; set; }

    }
}

注意看构造函数的这部分代码

            if (_currentUser != null)
            {
                //这里是核心,主要判断当前的开关,当前登录用户的
                var limit = _currentUser.FindClaim("LimitUser")?.Value.To<bool>();
                if (limit.HasValue && limit.Value)
                {
                    var userid = _currentUser.FindClaim("UserId")?.Value.To<int>();
                    LimitUserId = true;
                    CurrentUserId = userid.Value;
                }
            }

这里主要就是判断当前用户是否需要过滤!

然后就是

        /// <summary>
        /// 
        /// </summary>
        /// <param name="builder"></param>
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.ConfigurePasteTest();

            foreach (var entityType in builder.Model.GetEntityTypes())
            {
                if (typeof(IUserIdEntity).IsAssignableFrom(entityType.ClrType))
                {
                    MyPropertiesMethodInfo.MakeGenericMethod(entityType.ClrType).Invoke(this, new object[] { builder, entityType });
                }
            }
        }

看看哪些Entity实现了IUserIdEntity

TestTable

按照上面的,我用一个测试表来测试下

    /// <summary>
    /// 测试表
    /// </summary>
    public class MyLinkTable : Entity<int>, IUserIdEntity
    {
        /// <summary>
        /// 名称
        /// </summary>
        [MaxLength(16)]
        public string Name { get; set; }

        /// <summary>
        /// 年龄
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        /// 归属
        /// </summary>
        public int UserId { get; set; }
    }

关联当前

上面的代码感觉是不是少了啥,看到LimitUser,是不是少了一个写入的地方!!!

这个是我自定义的一个信息,所以我们需要在合适的地方写入!!!

假设有这么一个函数

        /// <summary>
        /// 获取
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet]
        [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "data", "edit" })]
        public async Task<PagedResultDto<MyLinkTableListDto>> Page([FromQuery] InputQueryMyLinkTable input)
        {
            var _query = from a in _dbContext.MyLinkTable

                         select new MyLinkTableListDto
                         {
                             Name = a.Name,
                             Age = a.Age,
                             UserId = a.UserId,
                             Id = a.Id
                         };
            _query = _query.WhereIf(!string.IsNullOrEmpty(input.word), x => x.Name.Contains(input.word));
            var _pagedto = new PagedResultDto<MyLinkTableListDto>();
            if (input.page == 1)
            {
                _pagedto.TotalCount = await _query.CountAsync();
            }
            var dataList = await _query
                .OrderByDescending(x => x.Id)
                .Page(input.page, input.size)
                .AsNoTracking()
                .ToListAsync();
            if (dataList == null || dataList.Count == 0)
            {
                return null;// throw new PasteCodeException("没有查询到数据", 204);
            }
            _pagedto.Items = dataList;
            return _pagedto;
        }

查询不是在函数体么,所以我们在他之前实现 LimitUser 即可,也就是过滤器中!

RoleAttribute

在我上面的案例中,我只要在这个过滤器中按需写入即可

                    if (!token.CurrentIsRoot())
                    {
                        var claims = new[] { new Claim("LimitUser", "true"), new Claim("UserId", token.CurrentUserId().ToString()) };
                        var identity = new ClaimsIdentity(claims, "Current");//Simple
                        context.HttpContext.User = new ClaimsPrincipal(identity);
                    }

在这里,你就可以做很多花样了,因为在这里你可以获得当前用户的非常多东西,比如IP,Headers,Cookie等
我这里的判断是,只要不是超级用户,都需要做限定!!!

总结

上面的代码可以分为2个部分,

  • 1.把控开关的地方RoleAttribute也就是写入LimitUser的地方
  • 2.实现LimitUser功能的地方

对于1来说,我不管你2如何实现,我1只管按照条件打开还是关闭开关即可
对于2来说,我不管1如何实现的,我只要按照对应的值干活即可!

测试效果

以上代码完成后,我们来运行看看
我先看看角色列表,这个角色表我是没有添加IUserIdEntity的,查询语句如下

SELECT "p"."Id", "p"."Code", "p"."Desc", "p"."IsEnable", "p"."Mark", "p"."Name"
FROM "PTGradeInfo" AS "p"
ORDER BY "p"."Id" DESC
LIMIT @__p_1 OFFSET @__p_0

再看看有添加IUserIdEntity的

SELECT "p"."Name", "p"."Age", "p"."UserId", "p"."Id"
FROM "PTMyLinkTable" AS "p"
WHERE @__ef_filter__p_0 OR ("p"."UserId" = @__ef_filter__CurrentUserId_1)
ORDER BY "p"."Id" DESC
LIMIT @__p_1 OFFSET @__p_0

看到区别没有?

WHERE @__ef_filter__p_0 OR ("p"."UserId" = @__ef_filter__CurrentUserId_1)

按照字面意思是添加了过滤了
我们加一些数据测试下!
数据的UserId字段我赋值的是当前用户
然后UserId=1的为超级用户
UserId=2的为非超级用户
我用超级用户查看如下

图片alt

切换到非超级用户,查看如下

图片alt

符合我的预期!!!

扩展

上面只是一个案例,实际中很多地方可以使用
比如上面说的 我的部门,我的店铺,我的创建等
你只要按照流程添加不同的接口,然后条件判断中添加对应的信息即可!!!

评论列表
尘埃
6 52 0
快捷注册
热门推荐更多
PasteTalk
是在线客服系统更是在线营销系统,引入特有的页面话序机制,能够针对不同访客实现丰富的营销话语,提高开发效率!引入分词功能提升关键字命中概率,提高服务质量!;
最新动态
  • 216.****.221 正在查看 论我是如何改造ABP官方的审计日志Auditing系统的! !
  • 19.****.153 正在查看 PasteSpider部署工具介绍,为啥说是开发专属部署工具,看这篇就够了! !
  • 19.****.153 正在查看 PasteSpider部署工具介绍,为啥说是开发专属部署工具,看这篇就够了! !
  • 24.****.118 正在查看 PasteSpider测试环境之为centos7中的ssh启用证书登陆模式 !
  • 24.****.118 正在查看 PasteSpider测试环境之为centos7中的ssh启用证书登陆模式 !
  • 216.****.208 正在查看 PasteSpider的V5正式版发布啦!(202504月版),更新说明一览 !
  • 216.****.208 正在查看 PasteSpider部署工具介绍,为啥说是开发专属部署工具,看这篇就够了! !
  • 216.****.208 正在查看 有时候发布,没反应,看任务列表也没有 !
  • 230.****.179 正在查看 以容器方式运行PasteSpider(PasteSpider的安装)一键拉取镜像 !
  • 230.****.179 正在查看 以容器方式运行PasteSpider(PasteSpider的安装)一键拉取镜像 !
  • 99.****.171 正在查看 开发者部署工具PasteSpiderV5新版本更新内容 !
欢迎加入QQ讨论群 296245685 [PasteSpider]介绍 [PasteForm]介绍 @2022-2023 PasteCode.cn 版权所有 ICP证 闽ICP备2021013869号-2