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

前端

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

request header add key X-Tenant

X-Tenant:0001

后端

1.定义多租户Provider

csharp 复制代码
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.数据库上下文设置多租户查询过滤和写入

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

typescript 复制代码
private string _tenantCode;
public DemoContext(DbContextOptions<DemoContext> options, IMultiTenantProvider multiTenantProvider) : base(options)
{
    _tenantCode= multiTenantProvider.GetTenantCode();
}

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

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

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

csharp 复制代码
public override Task<int> 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中

csharp 复制代码
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

arduino 复制代码
public static class MultiTenantExtension
{
    public static void AddMultiTenant(this IServiceCollection serviceCollection)
   {
       serviceCollection.AddScoped<IMultiTenantProvider,MultiTenantProvider>();
   }

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

5.使用说明

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

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

csharp 复制代码
DemoContext _context;
IServiceProvider _serviceProvider;

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

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

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

        var context = scope.ServiceProvider.GetRequiredService<DemoContext>();

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

进阶版 MultiTenantDbContext

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

csharp 复制代码
public class MultiTenantEntity
{
    /// <summary>
    /// 租户代码
    /// </summary>
    public string TenantCode { get; set; }
}

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

csharp 复制代码
public class MultiTenantDbContext : DemoContext
    {
        private string _tenantCode;
        public MultiTenantDbContext (IMultiTenantProvider multiTenantProvider, DbContextOptions<DemoContext> options) : base(options)
        {
            _tenantCode= multiTenantProvider?.GetTenantCode();
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            //批量对所有继承于MultiTenantEntity的实体进行租户查询条件的过滤
            //AddQueryFilter扩展方法的实现代码在后面
            modelBuilder.AddQueryFilter<MultiTenantEntity>(x => x.TenantCode == _tenantCode);
        }
        public override Task<int> 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的实体进行租户查询条件的过滤? 可以采用下述的扩展方法

csharp 复制代码
public static class EntityFrameworkExtensions
    {
        public static void AddQueryFilter<T>(this ModelBuilder modelBuilder,
            Expression<Func<T, bool>> 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);
            }
        }
    }

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

vbnet 复制代码
public static IServiceCollection AddDemoDbContext(this IServiceCollection services, IConfiguration configuration)
        {
            if (configuration["IsMultiTenantMode"] == "true")
            {
                services.AddDbContext<DemoContext, MultiTenantDbContext>(option => option.UseNpgsql(configuration["DbconnectString:Control"]));
            }
            else
            {
                services.AddDbContext<DemoContext>(option => option.UseNpgsql(configuration["DbconnectString:Control"]));
            }
            return services;
        }
相关推荐
时光追逐者2 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 61 期(2025年11.10-11.16)
c#·.net·.netcore
2509_940880222 小时前
【update 更新数据语法合集】.NET开源ORM框架 SqlSugar 系列
开源·.net
葛小白15 小时前
Labview实用04:Labview调用.net中的事件
.net·labview
唐青枫8 小时前
.NET Web 应用 Linux 部署全指南:从环境搭建到生产上线
c#·.net
mudtools1 天前
.NET驾驭Excel之力:工作簿与工作表操作基础
c#·.net·excel
mudtools1 天前
.NET驾驭Excel之力:单元格与区域操作详解
c#·.net·excel
专注VB编程开发20年1 天前
.net按地址动态调用VC++DLL将非托管DLL中的函数地址转换为.NET可调用的委托
开发语言·c++·c#·.net
追逐时光者1 天前
一个基于 .NET WPF 开源的本地硬盘千万级图库以图搜图小工具!
后端·.net
唐青枫1 天前
C#.NET 全局异常到底怎么做?最完整的实战指南
c#·.net