在做Saas应用时,多租户解析往往是很重要的组成部分,也是用户访问网站最先处理的逻辑。
文前介绍:
多租户的数据库实现方式主要有三种:
- 单一数据库实现,每条数据标识租户Id进行识别数据属于哪个租户
- 一租户一个数据库,能够做到完全的数据隔离
- 混合模式,部分数据在一张表上,主要是一些基础数据;其他业务数据分库存储。
无论是哪种方式都要知道租户是谁才能查询数据库。
获取租户的方式也可以有多种:
- 根据域名或者子域名不同可以获知租户,上面的所有场景都适合使用;(必须要有域名和dns服务)
- 根据用户id获取租户信息,从而存入前端的cookie中或者header中,只适用于上面的1、3方式,因为需要不同租户的用户都存入一种表中,方便查询对应的租户信息;
下文中的例子只是简单的一个例子,没有进一步的业务场景,实现功能如下:
用户根据不同的域名进入系统;
后台拿刀http传入的域名并解析;
查询租户数据库,查找出租户名称返回显示。
新建解决方案,并其下新增三个项目:
try_MultiTenantApi:webapi项目,项目的启动项目,为了方方便,租户的Service和IService放入了这个项目中,实际应用时要放入业务层;
MultiTenantApi.Models:类库项目,存放租户对象;
MultiTenantApi.Data:数据层,使用efcore,存放上下文DbContext和迁移文件
MultiTenantApi.Models中新增实体Tenant
```
public class Tenant
{
public int Id { get; set; }
public string Identifier { get; set; } // 租户标识符,例如域名
public string Name { get; set; }
public string ConnectionString { get; set; } // 每个租户的独立数据库连接字符串
}
```
MultiTenantApi.Data:
引入nuget包:要注意跟你项目的.net版本相同
这些包的具体说明:
-
Microsoft.EntityFrameworkCore
- 功能: Entity Framework Core 是一个现代的对象-关系映射器(ORM),用于 .NET。它支持 LINQ 查询、变更跟踪、更新和模式迁移。EF Core 可以与多种数据库一起工作,包括 SQL Server、Azure SQL Database、SQLite 和 Azure Cosmos DB。
- 作用: 提供了核心的 ORM 功能,用于在 .NET 应用程序中与数据库进行交互。
-
Microsoft.EntityFrameworkCore.Design
-
功能: 提供了 Entity Framework Core 工具的共享设计时组件。这些组件包括用于创建和管理迁移的工具。
- 作用: 用于在设计时(如运行迁移命令时)提供必要的工具支持,帮助开发者更高效地管理和生成数据库迁移。
-
Microsoft.EntityFrameworkCore.Proxies
-
功能: 为 Entity Framework Core 提供延迟加载代理。这些代理用于在需要时加载相关对象,从而减少内存使用和提高性能。
- 作用: 通过代理机制实现延迟加载,提高应用程序的性能和响应速度。
-
Microsoft.EntityFrameworkCore.SqlServer
-
功能: 提供了针对 Microsoft SQL Server 的数据库提供程序。这使得 Entity Framework Core 能够与 SQL Server 数据库进行交互。
- 作用: 为 SQL Server 数据库提供特定的数据库提供程序,确保 EF Core 能够正确地与 SQL Server 交互。
-
Microsoft.EntityFrameworkCore.Tools
-
功能: 提供了 Entity Framework Core 工具,用于 NuGet Package Manager Console 中的 Visual Studio。
- 作用: 通过 Visual Studio 的 NuGet Package Manager Console 提供 EF Core 工具,帮助开发者更方便地管理和使用 EF Core 的设计时工具。
新增数据上下文:
```
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet Tenants { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity().HasData(
new Tenant { Id = 1, Identifier = "tenant1", Name = "Tenant 1", ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=TenantDb1;Trusted_Connection=True;" },
new Tenant { Id = 2, Identifier = "tenant2", Name = "Tenant 2", ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=TenantDb2;Trusted_Connection=True;" }
);
}
}
```
注意实际应用中上面的数据库连接字符串根据你的具体情况变动一下,因为本例子没有用到这个字段所以想要尝试可以不用改。
try_MultiTenantApi:
引入nuget包:
新建类TenantService:
```
public interface ITenantService
{
Task GetTenantByIdentifierAsync(string identifier);
}
public class TenantService : ITenantService
{
private readonly ApplicationDbContext _context;
public TenantService(ApplicationDbContext context)
{
_context = context;
}
public async Task GetTenantByIdentifierAsync(string identifier)
{
return await _context.Tenants.FirstOrDefaultAsync(t => t.Identifier == identifier);
}
}
```
新建一个中间件TenantMiddleware:
用于每次的请求解析出租户信息
```
namespace try_MultiTenantApi
{
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
{
var subdomain = GetSubdomain(context.Request.Host.Value);
if (!string.IsNullOrEmpty(subdomain))
{
var tenant = await tenantService.GetTenantByIdentifierAsync(subdomain);
if (tenant != null)
{
context.Items["Tenant"] = tenant;
}
}
await _next(context);
}
private string GetSubdomain(string host)
{
var parts = host.Split('.');
return parts.Length > 2 ? parts[0] : null;
}
}
}
public static class TenantMiddlewareExtensions
{
public static IApplicationBuilder UseTenantMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
```
配置appsetting文件,配置租户数据库连接字符串:
```
"ConnectionStrings": {
"DefaultConnection": "Data Source=localhost;Initial Catalog=try_MultiTenant;User ID=sa;Password=******;Encrypt=False;"
},
```
修改Program:
```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseTenantMiddleware(); // 使用租户解析中间件
app.MapControllers();
app.Run();
```
新建一个controller:ValuesController
```
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly ITenantService _tenantService;
public ValuesController(ITenantService tenantService)
{
_tenantService = tenantService;
}
[HttpGet(nameof(Get1))]
public async Task Get1()
{
var tenant = HttpContext.Items["Tenant"] as Tenant;
if (tenant == null)
{
return NotFound("Tenant not found");
}
return Ok(new { Message = $"Hello from {tenant.Name}" });
}
}
```
OK,至此项目就编码完成,接下来就是进行数据迁移。因为前面上下文中已经设置了出事测试数据
add-migration init
update-database
到此,租户的数据库已经创建,并且也有了初始的测试数据:
为了测试方便咱们在launch启动文件中配置两个租户的访问地址:
```
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5252;http://tenant1.Paas.JBWL.com:5252",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
}
```
接下来设置try_MultiTenantApi项目为启动项目,启来后出swagger页面:
页面地址是默认第一个http://localhost:5252/
下面我们就可以修改一下本地的host文件映射两个域名,可以使用swichhost,也可以找到本地的host文件,增加地址映射
```
127.0.0.1 tenant1.Paas.JBWL.com
127.0.0.1 tenant2.Paas.JBWL.com
```
启用后访问
```
tenant1.Paas.JBWL.com:5252
```
出现前面一模一样的页面,访问一下测试接口看是否解析成功:
成功!
接下来试一下tenant2.paas.jbwl.com:5252
至此,这个测试demo也就完成了,只要解析出租户信息,接下来Saas的租户数据就能获取了。