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

Viewed 1447

I have to create a RESTful Web API (I'm using ASP.NET Core 1.1) for a system which is maybe a bit unusual in the sense that each client using this system has their own database. (A client is like a company, with many users in it). So there is a master database which specifies, for each client, which database it must connect to. All clients databases are identical (except for the data of course).

I'm wondering how I should handle this... Normally, when an app consumes a Web API, you might have something like this:

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;
    }
}

Now, in the above example, the problem is that the Context has to point to different databases, depending on which client is requesting the list of Accounts. I'm not sure how to best tackle this. Would it be OK if the front end sends, with every API call, a client identifier in a query string? So every single controller action would have a ClientId parameter. And based on this parameter, the action would modify the Context's connection string on the fly, just before calling it? It just sounds terrible though - both from an implementation and a security point of view. The problem is that, as far as I'm aware, ASP.NET is stateless, so every time the API is called, the API will have no idea who this is?

Or how else could I do this?

2 Answers

@Srivaishnav

What I did is, each client sends a tenantId in the header of each request to the Web API. Then, the Web API reads this tenantId in some middleware, and saves it in the HttpContext so that it's readable anywhere in the API. Then, in my DbContext, in the OnConfiguring method, I read the tenantId from the HttpContext, and use it to create the connection string for that DbContext. The code:

TenantIdentifier.cs middleware:

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();
    }

I've simplified the example a bit, but that's pretty much the crux of it. Let me know if you have any questions... Good luck!

Related