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