多租户 .Net Core Web API

3
我需要建立一个Web API来服务于现有系统,全国各地都有不同的机构,每个机构都有它自己的数据库。所有数据库都在单个服务器上,所有的数据库结构相同,但是每个数据库都有它自己的用户名和密码。每个机构都有一个或多个用户,而一个用户可以属于一个或多个机构。还有一个特殊的数据库,其中包含了所有用户和机构的表格以及用户-机构桥接表。

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

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

因此,web应用程序将类似于Windows应用程序的功能。用户登录到web应用程序。消耗我的Web API的Web应用程序告诉API刚刚登录的是哪个用户。然后,API从存储这些数据的数据库检查此用户属于哪些机构。API将用户所属的机构列表返回给Web应用程序。用户在那里选择一个机构。从此时起,Web应用程序将在所有API调用的标题中包括此机构ID。当API接收到来自Web应用程序的请求时,它会根据请求头中的机构ID知道要使用哪个数据库。

希望这样讲述清楚了...

显然,这意味着我将不得不根据API需要连接的数据库动态更改DbContext的连接字符串。我一开始在控制器本身上实现这一点,并成功运行,但这将涉及到在所有控制器中进行大量复制和粘贴反模式。因此,我尝试将其移动到DbContext的OnConfiguring事件中。我认为最好的方法是创建一个DbContext工厂来创建DbContext,在其中使用适当的连接字符串。但是我有点迷茫。您看,当web应用程序调用Web API的端点(例如HTTP GET请求以获取账户列表)时,将触发Accounts控制器中的HttpGet处理程序。此操作方法然后读取机构ID头。但这一切都是在控制器上发生的...如果我从DbContext的OnConfiguring()事件中调用DbContext工厂,它将不得不将已在控制器中读取的机构ID发送到工厂,以便工厂知道要创建哪个连接字符串。我试图不使用全局变量来保持我的类松散耦合。

除非我在管道中运行某个服务,拦截所有请求,读取机构ID头,然后将其注入到DbContext构造函数中?不知道该如何实现...

总的来说,我有些迷茫。我甚至不确定这是否是正确的方法。我看过一些“多租户”示例,但老实说,我觉得它们有点难以理解,我希望现在能做一些更简单的事情,并且随着我对 .Net Core 的认识提高,我可以相应地改进代码。


请提供需要翻译的英文内容。 - Riscie
你看过 SaasKit 了吗? - Tseng
1个回答

3

我正在处理与您在此处描述的类似事物。由于我也刚开始,所以我还没有万全之策。但有一件事情可以帮助您的方法:

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

我采用了让一个中间件负责交换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;
    }
}

我使用一个自定义的http头部叫做X-Tenant-Guid来识别租户GUID。然后,我向全局数据库发出请求,获取此租户数据库的连接字符串。
我在这里公开了示例:https://github.com/riscie/ASP.NET-Core-Multi-Tenant-multi-db-Example(它还没有更新到asp net core 2.1,但很快就可以解决)。

谢谢Riscie!我正在下载你的Github项目。完成后我会告诉你进展如何... - Fabricio Rodriguez
当然可以使用任何您喜欢的方法来识别租户,它不一定是HTTP头。子域名也是另一种流行的标识符。 - Riscie
好的,我大部分已经解决了。只是在OnConfiguring事件上出现了一个错误。第二次它触发时,会出现这个错误:“已经添加了具有相同键的项。键:Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal.MySqlOptionsExtension”(我正在使用Pomelo for MySql)- 这可能是Pomelo的问题。我明天会进一步调查。谢谢。 - Fabricio Rodriguez
我使用的方法几乎相同,但是当您拥有全局用户数据库并且希望将所有“用户”特定操作存储在租户数据库中时,您将如何解决问题。用户不应该驻留在租户数据库本身吗?跨数据库连接不是一个好主意。 - timeshift
TenantID可以作为声明(如果您正在使用它们)放入JWT令牌中。这将使客户端更加清晰,因为他们不必担心传递另一个标头。 - Robert Perry
是的,@RobertPerry,你说得对。这是另一种方法。在我的情况下,我最终将有关用户活动租户的信息存储在我的应用程序数据库中,紧挨着用户。 - Riscie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接