如何根据客户端调用实现具有多个数据库的 Web API

How to implement Web API with many databases depending on client calling

我必须为一个系统创建一个 RESTful Web API(我正在使用 ASP.NET Core 1.1),从每个客户端都使用这个系统有自己的数据库。 (客户就像一家公司,里面有很多用户)。所以有一个主数据库,它为每个客户端指定它必须连接到哪个数据库。所有客户端数据库都是相同的(当然数据除外)。

我想知道我应该如何处理这个...通常,当应用程序使用 Web API 时,您可能会遇到这样的事情:

public class AccountsController : Controller
{
    private readonly SomeContext _context;

    public FilesController(SomeContext context)
    {
        _context = context;
    }

    // GET: api/Files
    [HttpGet)]
    public IEnumerable<Account> GetAccounts()
    {
            return _context.Accounts;
    }
}

现在,在上面的示例中,问题是上下文必须指向不同的数据库,这取决于哪个客户端正在请求帐户列表。我不确定如何最好地解决这个问题。如果前端在每次 API 调用时在查询字符串中发送一个客户端标识符,是否可以?所以每个控制器动作都会有一个 ClientId 参数。并且基于此参数,该操作会在调用它之前即时修改 Context 的连接字符串?不过,这听起来很糟糕——无论是从实现还是安全的角度来看。问题是,据我所知,ASP.NET 是无状态的,所以每次调用 API 时,API 都不知道这是谁?

或者我还能怎么做?

我将从访问令牌中识别用户(以及她希望使用的数据库)。

@Srivaishnav

我所做的是,每个客户端在每个请求的 header 中向 Web API 发送一个 tenantId。然后,Web API 在一些中间件中读取这个 tenantId,并将其保存在 HttpContext 中,以便它在 API 中的任何地方都可读。然后,在我的 DbContext 中,在 OnConfiguring 方法中,我从 HttpContext 中读取 tenantId,并使用它为该 DbContext 创建连接字符串。代码:

TenantIdentifier.cs中间件:

public class TenantIdentifier
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _memoryCache;
    private DbContext _dbContext;        

    public TenantIdentifier(RequestDelegate next, IMemoryCache memoryCache)
    {
        _next = next;
        _memoryCache = memoryCache;
    }

    public async Task Invoke(HttpContext httpContext, DbContext dbContext)
    {
        _dbContext = dbContext;

        string clientIdHeader = httpContext.Request.Headers["clientId"].FirstOrDefault();
        if (string.IsNullOrEmpty(clientIdHeader))
        {
            await _next.Invoke(httpContext);
            return;
        }                

        int clientId = int.Parse(clientIdHeader);

        httpContext.Items["CLIENT"] = clientId;

        await _next.Invoke(httpContext);
    }

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

DbContext.cs:

    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantContext(DbContextOptions<DbContext> options, IHttpContextAccessor httpContextAccessor)
        : base(options)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (_httpContextAccessor == null)
            return;

        int? clientId = (int?)_httpContextAccessor.HttpContext.Items["CLIENT"];
        if (!client.HasValue)
        {
            optionsBuilder = null;
            return;
        }

        string connectionString = null;

        switch (tenantId.Value)
        {
            case 1:
                connectionString = "some connection string";
                break;

            case 2:
                connectionString = "some other connection string";
                break;
        }

        optionsBuilder.UseMySql(connectionString);
    }

Startup.cs

    public void Configure(IApplicationBuilder app)
    {
        app.UseTenantIdentifier();
        app.UseMvc();
    }

我已经稍微简化了这个例子,但这几乎就是它的症结所在。如果您有任何问题,请告诉我...祝您好运!