【问题标题】:Multi-tenant .Net Core Web API多租户 .Net Core Web API
【发布时间】:2018-11-13 13:54:51
【问题描述】:

我需要为现有系统构建 Web API。全国各地都有各种机构,每个机构都有自己的数据库。所有数据库都在一台服务器上。所有数据库的结构都相同。所有数据库都有自己的用户名和密码。一个机构有一个或多个用户。一个用户可以属于一个或多个机构。还有一个特殊的数据库,其中包含一个所有用户表、一个所有代理表和一个用户代理桥表。

目前他们使用的是传统的 Windows 桌面应用程序。当用户设置此 Windows 程序时,他们使用用户名和密码登录。然后系统会为他们显示他们所属的所有机构的列表(通常只有一个,但一些“高级用户”可能属于少数几个)。他们选择一个机构,然后程序连接到正确的数据库。在会话的剩余时间里,用户所做的一切都将在该数据库上完成。

客户想要创建一个 Web 应用程序来最终替换 Windows 程序(两者将并行运行一段时间)。一位开发人员正在 Angular 5 中创建前端,而我正在 ASP .Net Core 2.1 中开发 API。

因此,Web 应用程序将以与 Windows 应用程序类似的方式运行。用户登录到 Web 应用程序。使用我的 Web API 的 Web 应用程序告诉 API 哪个用户刚刚登录。API 然后从存储该数据的数据库中检查该用户属于哪个机构。 API 返回用户所属的网络应用程序的代理列表。在那里,用户选择一个机构。从此时起,Web 应用程序将在所有 API 调用的标头中包含此代理 ID。 API 在收到来自 Web 应用程序的请求时,将根据请求标头中的代理 ID 知道要使用哪个数据库。

希望这是有道理的......

显然,这意味着我将不得不即时更改 DbContext 的连接字符串,具体取决于 API 必须与哪个数据库通信。我一直在研究这个问题,首先是在控制器本身上进行,这很有效,但会在我的所有控制器中涉及大量复制和粘贴反模式。所以我试图将其移至 DbContext 的 OnConfiguring 事件。我在想最好使用适当的连接字符串创建一个 DbContext 工厂来创建 DbContext。我只是有点失落。您会看到,当 Web 应用程序调用 Web api 上的端点时(假设是一个 HTTP GET 请求以获取帐户列表),这将触发 Accounts 控制器中的 HttpGet 处理程序。然后,此操作方法读取代理 ID 标头。但这一切都发生在控制器上......如果我从 DbContext 的 OnConfiguring() 事件中调用 DbContext 工厂,它必须将代理 ID(在控制器中读取)发送到工厂,以便工厂知道要创建的连接字符串。我试图不使用全局变量来保持我的类松散耦合。

除非我在管道中运行了一些服务来拦截所有请求,读取代理 ID 标头,并且这以某种方式被注入到 DbContext 构造函数中?不知道我会怎么做...

总之,我有点迷茫。我什至不确定这是否是正确的方法。我看过一些“多租户”示例,但老实说,我发现它们有点难以理解,我希望我现在可以做一些更简单的事情,随着时间的推移,据我所知.Net Core 的改进,我可以看看相应地改进代码。

【问题讨论】:

  • 见下面我的回答。如果您对样本有任何疑问,请告诉我。
  • 你看过SaasKit吗?

标签: asp.net-core multi-tenant asp.net-core-webapi


【解决方案1】:

我正在研究与您在此处描述的类似的东西。由于我还处于起步阶段,所以我还没有灵丹妙药。不过,有一件事可以帮助您采用您的方法:

首先在控制器本身上执行此操作,这可行,但会在我的所有控制器中涉及大量复制粘贴反模式。

我采用了一个中间件来负责交换 dbconnection 字符串的方法。像这样的:

public class TenantIdentifier
{
    private readonly RequestDelegate _next;

    public TenantIdentifier(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, GlobalDbContext dbContext)
    {
        var tenantGuid = httpContext.Request.Headers["X-Tenant-Guid"].FirstOrDefault();
        if (!string.IsNullOrEmpty(tenantGuid))
        {
            var tenant = dbContext.Tenants.FirstOrDefault(t => t.Guid.ToString() == tenantGuid);
            httpContext.Items["TENANT"] = tenant;
        }

        await _next.Invoke(httpContext);
    }
}


public static class TenantIdentifierExtension
{
    public static IApplicationBuilder UseTenantIdentifier(this IApplicationBuilder app)
    {
        app.UseMiddleware<TenantIdentifier>();
        return app;
    }
}

这里我使用了一个名为X-Tenant-Guid 的自行创建的http-header 来识别租户GUID。然后我向全局数据库发出请求,在那里我得到了这个租户数据库的连接字符串。

我在这里公开了这个例子。 https://github.com/riscie/ASP.NET-Core-Multi-Tenant-multi-db-Example (它还没有更新到asp net core 2.1,但是这么快应该没有问题)

【讨论】:

  • 谢谢瑞西!我现在正在下载你的 Github 项目。会让你知道进展如何......
  • 当然!您当然可以使用任何您喜欢的方法来识别租户。它不必是 http 标头。子域例如。是另一个流行的标识符。
  • 好的,我基本上可以正常工作了。我只是在 OnConfiguring 事件中遇到错误。第二次触发时,出现此错误“已添加具有相同密钥的项目。密钥:Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal.MySqlOptionsExtension”(我正在使用 Pomelo for MySql) - 它可能是柚子的事。明天我会进一步调查。谢谢
  • 我使用了几乎相同的方法,但是当您拥有一个全局用户数据库并且您希望将所有“用户”特定的操作存储在租户数据库中时,您将如何解决这些问题。用户不应该在租户数据库本身中使用吗?跨数据库连接不会是一件好事。
  • TenantID 可以作为声明进入 JWT 令牌内部(如果您正在使用它们)。这将使客户端更清洁,因为他们不必担心传递另一个-标题..