I have applied the multi-tenant location for EF Core database applying customize schema per tenant in my ASP.NET Core Web API project.
To do that the client send request through the API by setting header X-Tenant-Id caught like this:
// NOTICE: Scoped service for tenant resolution
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.Request.Headers["X-Tenant-Id"];
return !string.IsNullOrEmpty(tenantIdString) && Guid.TryParse(tenantIdString, out var tenantId) ? new Tenant { TenantId = tenantId } : null;
});
After that, I inject in scoped lifetime, the database context with the client schema saved in application database tables like this :
// NOTICE: Scoped service for ClientDbContext resolution
builder.Services.AddScoped(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var applicationDbContext = sp.GetRequiredService<ApplicationDbContext>();
var tenant = sp.GetRequiredService<ITenant>();
if (EF.IsDesignTime)
{
return new ClientDbContext(new DbContextOptions<ClientDbContext>(), configuration);
}
var client = applicationDbContext.Clients.FirstOrDefault(x => x.TenantId == tenant.TenantId);
return new ClientDbContext(configuration, client?.Schema ?? throw new ClientNotDeclaredException());
});
The creation of client tables per schema works pretty well but I want to throw an exception when an undeclared client try to access the system. That is why there is throw new ClientNotDeclaredException() at the end.
Technical stack:
- .NET 8.0
- EF Core 9.0.9
I have tried to handle the exception in the controller and ApiBehavior using InvalidModelStateResponseFactory without success.
The objective
I want to catch the exception to render HTTP 403 Forbidden instead HTTP 500 Server Error but I don't know how it can be possible to handle it and furthermore render the right HTTP status.
Do you know how to do that or have any idea?
1 Answer 1
As suggested by @Ivan Petrov, I search another location to throw the exception in the aim to handle it properly.
And I found it. Due to multi-tenant, I have a default schema definition for EF design time. So I can detect if we want to generate tables with default schema or not. And I can throw the exception at its location instead in Scope injection.
protected override void OnModelCreating(ModelBuilder builder)
{
if(!EF.IsDesignTime && Schema == DefaultSchema)
throw new ArgumentException("There is no data accessible");
...
}
I replace my code in Scope injection like this
//NOTICE: Scoped service for ClientDbContext resolution
builder.Services.AddScoped(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var applicationDbContext = sp.GetRequiredService<ApplicationDbContext>();
var tenant = sp.GetRequiredService<ITenant>();
if (EF.IsDesignTime)
{
return new ClientDbContext(new DbContextOptions<ClientDbContext>(), configuration);
}
var client = applicationDbContext.Clients.FirstOrDefault(x => x.TenantId == tenant.TenantId);
return new ClientDbContext(configuration, client?.Schema ?? ClientDbContext.DefaultSchema);
});
With this code, I can handle it in controller and use the default exception handler.
Thanks for help
Comments
Explore related questions
See similar questions with these tags.
ClientDbContext. Maybe you can create some ClientDbContextFactory instead as dependency that will make things a bit easier on the controller level.