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 inappsettings.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 customerDBContextTenant
- this context will have a connection string inappsettings
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);