Home > Software engineering >  How to load dynamic dbcontext based on database name obtained from another dbcontext?
How to load dynamic dbcontext based on database name obtained from another dbcontext?

Time:09-22

I'm developing a Web API application with ASP.NET Core 5 with the goal of being multitenant capable. Each tenant will have their own database.

In this application I will have two DbContext:

  • DBContextMaster - this context data will be static in appsettings.json. It will get the information of the tenant making the request from the Web API and get the name of the database stored for that specific customer

  • DBContextTenant - this context will have a connection string in appsettings similar to this:

      "DynamicTenant": "Data Source=server;Initial Catalog={dbName};Integrated Security=False.
    

    {dbName} must be replaced in order for the correct database to be used.

I have some questions about how to do this, but my idea would be:

  • send the tenant code or a kind of APIKEY with the request
  • validate if the tenant exists and then replace the DynamicTenant connection string (deploy some kind of cache to avoid successive queries checking if the same tenant exists)

As I am just starting with ASP.NET Core, I would like to know how and where I could dynamically load the tenant context.

UPDATE:

This code will apparently serve me well.

   services.AddDbContext<DbContextTenant>((serviceProvider, dbContextBuilder) =>
        {
            var connectionStringPlaceHolder = Configuration.GetConnectionString("DynamicTenant");
            var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
            var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First();
            var connectionString = connectionStringPlaceHolder.Replace("{dbName}", dbName);
            dbContextBuilder.UseSqlServer(connectionString);

        });
        

But now I have another problem. When running the project there is no error, but when trying to add a new controller using this dbcontext, the following error occurs "Object reference not set to an instance of an object". Same error when I try to enable Migrations for the project.

I assume the error is occurring because at development time there is no http context.

What implementation can be done to get around these problems?

CodePudding user response:

send the tenant code or a kind of APIKEY with the request

You are doing right.

there are ways of getting tenants from users, you can use headers, you can use a query string, you can use a host (for example IP address or domain name) or a parameter to all your actions for example an API key.

but I suggest you, use host or header, so you can add a middleware for that to monitor all requests.

Look at this example:

public class TenantMiddleware
{

    private readonly RequestDelegate _next;

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

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


    public async Task Invoke (HttpContext context)
    {
        context.Request.Headers.TryGetValue(_tenantKey, out StringValues host);
        var tenantHolder = context.RequestServices.GetRequiredService<ITenantHolder>();
        tenantHolder.SetTenant(_tenantKey);
    }

}

tenantHolder is a scoped service that has the tenantId for each request and you can get the data from that. it just has a variable that gives you the tenant, you can also use it as a factory for your contexts.

you can find more details on this repo: TenantProvider

CodePudding user response:

Send tenant code in each request is correct.

If you use only one connection string for request and you pass the neme on Header you can do this:

services.AddDbContext<ContextDb>(options =>
            {
                var context = services.BuildServiceProvider().GetRequiredService<IHttpContextAccessor>().HttpContext;
                // get key from HttpContext and create your connectionString

                options.UseSqlServer(connectionString)
                .LogTo(Console.WriteLine)
                .EnableSensitiveDataLogging();
            },
            ServiceLifetime.Scoped);
  • Related