1

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?

marc_s
760k186 gold badges1.4k silver badges1.5k bronze badges
asked Oct 6, 2025 at 20:44
2
  • 2
    controller is too late, you need to plug some custom exception handler before controllers are constructed (their dependencies built such as your ClientDbContext. Maybe you can create some ClientDbContextFactory instead as dependency that will make things a bit easier on the controller level. Commented Oct 6, 2025 at 21:03
  • 1
    Thanks @Ivan Petrov, I relocate the throw as I write in my answer below and I can handle it properly Commented Oct 7, 2025 at 8:21

1 Answer 1

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

answered Oct 7, 2025 at 8:19
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.