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;
        }
相关推荐
WineMonk1 天前
.NET WPF CommunityToolkit.Mvvm框架
.net·wpf·mvvm
界面开发小八哥1 天前
界面控件DevExpress WPF中文教程:Data Grid——卡片视图设置
.net·wpf·界面控件·devexpress·ui开发
九鼎科技-Leo1 天前
了解 .NET 运行时与 .NET 框架:基础概念与相互关系
windows·c#·.net
九鼎科技-Leo2 天前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
.net开发2 天前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
九鼎科技-Leo2 天前
在 C# 中,ICollection 和 IList 接口有什么区别?
windows·c#·.net
时光追逐者2 天前
C#/.NET/.NET Core学习路线集合,学习不迷路!
开发语言·学习·c#·asp.net·.net·.netcore·微软技术
Crazy Struggle2 天前
.NET 全功能流媒体管理控制接口平台
.net·开源项目·流媒体