0

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?

1

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

2 Comments

I would not want to use external library, as apparently this is an issue with a simpler solution
@Marcoscdoni it is not an external library, I had your issue before and I solved it by myself. that repo is written by me. it is not a library. it is a GitHub repo. you can check the sources, I did it by strategy pattern and it supports two ways of getting tenant
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.