EFCore多租户实现共享数据库模式

2023年 10月 13日 159.5k 0

前端

前端可根据当前登录的用户所属的租户,在请求头统一增加租户参数,也可由后端网关或中间件来统一获取当前用户的租户代码,本文重点介绍后端相关实现,此处不再赘述。

request header add key X-Tenant

X-Tenant:0001

后端

1.定义多租户Provider

public interface IMultiTenantProvider
{
     string GetTenantCode();
     void SetTenantCode(string tenantCode);
}
class MultiTenantProvider : IMultiTenantProvider
{
    private string _tenantCode;
    public string GetTenantCode()
    {
        return _tenantCode;
    }

    public void SetTenantCode(string tenantCode)
    {
        _tenantCode = tenantCode;
    }
}

2.数据库上下文设置多租户查询过滤和写入

通过构造函数获取当前的租户代码

private string _tenantCode;
public DemoContext(DbContextOptions options, IMultiTenantProvider multiTenantProvider) : base(options)
{
    _tenantCode= multiTenantProvider.GetTenantCode();
}

对每个实体设置租户查询过滤条件

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //在这里设置租户化的表,需要租户的entity请继承EntityBase;这里可以反射批量设置
    modelBuilder.Entity().HasQueryFilter(e => e.TenantCode== _tenantCode);
    //...
}

重写SaveChanges和SaveChangesAsync方法,实现新增时对租户代码赋值

public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
    //重写SaveChangesAsync方法,当新增时,对租户代码赋值
    ChangeTracker.Entries().Where(e => e.State == EntityState.Added && e.Entity is EntityBase).ToList().ForEach(e => ((EntityBase)e.Entity).TenantCode= _tenantCode);
    return base.SaveChangesAsync();
}

3.HTTP多租户中间件

定义中间件,实现从请求头获取租户代码,并设置到Provider中

public class HTTPMultiTenantMiddleware
{
    private readonly RequestDelegate _next;
    public HTTPMultiTenantMiddleware(RequestDelegate next)
   {
       _next = next;
   }

    public async Task Invoke(HttpContext context, IMultiTenantProvider _multiTenantProvider)
   {
       context.Request.Headers.TryGetValue("X-TenantCode", out var tenantCode);
       _multiTenantProvider.SetTenantCode(tenantCode);

       await _next.Invoke(context);
   }
}

4.在APP Startup时AddMultiTenant和UseMultiTenant

public static class MultiTenantExtension
{
    public static void AddMultiTenant(this IServiceCollection serviceCollection)
   {
       serviceCollection.AddScoped();
   }

    public static IApplicationBuilder UseMultiTenant(this IApplicationBuilder app)
   {
       return app.UseMiddleware();
   }
}

5.使用说明

  • 若是HTTP请求的业务,则上述中间件默认做了租户化,包括查询和新增的业务;
  • 若是后台任务类,在获取数据库的DbContext之前,需要根据业务数据来拿到租户,设置IMultiTenantProvider;

下面是两种场景的伪代码示例:

DemoContext _context;
IServiceProvider _serviceProvider;

public MultiTenantController(DemoContext context, IServiceProvider serviceProvider)
{
    _context = context;
    _serviceProvider = serviceProvider;
}

/// 
/// 这时查询的是请求头传入的租户
/// 
/// 
[HttpGet("empls")]
public async Task GetEmployees()
{
    var empls= await _context.Employees.AsNoTracking().ToListAsync();
    return Ok(empls);
}

/// 
/// 这时查询的是30001的租户
/// 
///  
[HttpGet("empls-task")]
public async Task GetEmployeesByTask()
{
    var empls= await Task.Run(async () =>
                             {
      using (var scope = _serviceProvider.CreateScope())
    {
        var multiTenantProvider = scope.ServiceProvider.GetRequiredService();
        multiTenantProvider.SetTenantCode("30001");

        var context = scope.ServiceProvider.GetRequiredService();

        var empls= await context.Employees.AsNoTracking().ToListAsync();
        return empls;
    }
  });
    return Ok(empls);
}

进阶版 MultiTenantDbContext

上述改造对原有DBContext的代码侵入很大,可以采用下述方案。 通常会定义好多院区的base entity,假设如下所示

public class MultiTenantEntity
{
    /// 
    /// 租户代码
    /// 
    public string TenantCode { get; set; }
}

业务实体需要多租户化的继承MultiTenantEntity即可。 这时,我们可以定义一个多租户的 DBContext,继承你的业务的context,代码如下

public class MultiTenantDbContext : DemoContext
    {
        private string _tenantCode;
        public MultiTenantDbContext (IMultiTenantProvider multiTenantProvider, DbContextOptions options) : base(options)
        {
            _tenantCode= multiTenantProvider?.GetTenantCode();
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            //批量对所有继承于MultiTenantEntity的实体进行租户查询条件的过滤
            //AddQueryFilter扩展方法的实现代码在后面
            modelBuilder.AddQueryFilter(x => x.TenantCode == _tenantCode);
        }
        public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            SetChangeTracker();
            return base.SaveChangesAsync();
        }
        public override int SaveChanges()
        {
            SetChangeTracker();
            return base.SaveChanges();
        }

        private void SetChangeTracker()
        {
            ChangeTracker.Entries().Where(e => e.State == EntityState.Added && e.Entity is MultiTenantEntity).ToList().ForEach(e => ((MultiTenantEntity)e.Entity).TenantCode = _tenantCode);
        }
    }

问题的核心在于如何批量的对所有继承于MultiTenantEntity的实体进行租户查询条件的过滤? 可以采用下述的扩展方法

public static class EntityFrameworkExtensions
    {
        public static void AddQueryFilter(this ModelBuilder modelBuilder,
            Expression expression)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                if (!typeof(T).IsAssignableFrom(entityType.ClrType))
                    continue;

                var parameterType = Expression.Parameter(entityType.ClrType);
                var expressionFilter = ReplacingExpressionVisitor.Replace(
                    expression.Parameters.Single(), parameterType, expression.Body);

                var currentQueryFilter = entityType.GetQueryFilter();
                if (currentQueryFilter != null)
                {
                    var currentExpressionFilter = ReplacingExpressionVisitor.Replace(
                        currentQueryFilter.Parameters.Single(), parameterType, currentQueryFilter.Body);
                    expressionFilter = Expression.AndAlso(currentExpressionFilter, expressionFilter);
                }

                var lambdaExpression = Expression.Lambda(expressionFilter, parameterType);
                entityType.SetQueryFilter(lambdaExpression);
            }
        }
    }

最后,可以根据环境变量,在启动的时候,选择多租户模式还是普遍模式

public static IServiceCollection AddDemoDbContext(this IServiceCollection services, IConfiguration configuration)
        {
            if (configuration["IsMultiTenantMode"] == "true")
            {
                services.AddDbContext(option => option.UseNpgsql(configuration["DbconnectString:Control"]));
            }
            else
            {
                services.AddDbContext(option => option.UseNpgsql(configuration["DbconnectString:Control"]));
            }
            return services;
        }

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论