Autofac Multitenant Database Configuration

asp.net-core autofac entity-framework-core multi-tenant

Question

I have a base abstract context which has a couple hundred shared objects, and then 2 "implementation" contexts which both inherit from the base and are designed to be used by different tenants in a .net core application. A tenant object is injected into the constructor for OnConfiguring to pick up which connection string to use.

public abstract class BaseContext : DbContext
{
    protected readonly AppTenant Tenant;

    protected BaseContext (AppTenant tenant)
    {
        Tenant = tenant;
    }
}

public TenantOneContext : BaseContext
{
   public TenantOneContext(AppTenant tenant) 
        : base(tenant)
    {
    }
}

In startup.cs, I register the DbContexts like this:

services.AddDbContext<TenantOneContext>();
services.AddDbContext<TenantTwoContext>();

Then using the autofac container and th Multitenant package, I register tenant specific contexts like this:

IContainer container = builder.Build();

MultitenantContainer mtc = new MultitenantContainer(container.Resolve<ITenantIdentificationStrategy>(), container);

mtc.ConfigureTenant("1", config =>
{
    config.RegisterType<TenantOneContext>().AsSelf().As<BaseContext>();
});

mtc.ConfigureTenant("2", config =>
{
    config.RegisterType<TenantTwoContext>().AsSelf().As<BaseContext>();
});

Startup.ApplicationContainer = mtc;

return new AutofacServiceProvider(mtc);

My service layers are designed around the BaseContext being injected for reuse where possible, and then services which require specific functionality use the TenantContexts.

public BusinessService
{
   private readonly BaseContext _baseContext;

   public BusinessService(BaseContext context) 
   {
       _baseContext = context;
   }
}

In the above service at runtime, I get an exception "No constructors on type 'BaseContext' can be found with the constructor finder 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder'". I'm not sure why this is broken....the AppTenant is definitely created as I can inject it other places successfully. I can make it work if I add an extra registration:

builder.RegisterType<TenantOneContext>().AsSelf().As<BaseContext>();

I don't understand why the above registration is required for the tenant container registrations to work. This seems broken to me; in structuremap (Saaskit) I was able to do this without adding an extra registration, and I assumed using the built in AddDbContext registrations would take care of creating a default registration for the containers to overwrite. Am I missing something here or is this possibly a bug in the multitenat functionality of autofac?

UPDATE:

Here is fully runable repo of the question: https://github.com/danjohnso/testapp

Why is line 66 of Startup.cs needed if I have lines 53/54 and lines 82-90?

1
1
4/24/2018 4:30:24 PM

Accepted Answer

As I expected your problem has nothing to do with multitenancy as such. You've implemented it almost entirely correctly, and you're right, you do not need that additional registration, and, btw, these two (below) too because you register them in tenant's scopes a bit later:

        services.AddDbContext<TenantOneContext>();
        services.AddDbContext<TenantTwoContext>();

So, you've made only one very small but very important mistake in TenantIdentitifcationStrategy implementation. Let's walk through how you create container - this is mainly for other people who may run into this problem as well. I'll mention only relevant parts.

First, TenantIdentitifcationStrategy gets registered in a container along with other stuff. Since there's no explicit specification of lifetime scope it is registered as InstancePerDependency() by default - but that does not really matter as you'll see. Next, "standard" IContainer gets created by autofac's buider.Build(). Next step in this process is to create MultitenantContainer, which takes an instance of ITenantIdentitifcationStrategy. This means that MultitenantContainer and its captive dependency - ITenantIdentitifcationStrategy - will be singletons regardless of how ITenantIdentitifcationStrategy is registered in container. In your case it gets resolved from that standard "root" container in order to manage its dependencies - well, this is what autofac is for anyways. Everything is fine with this approach in general, but this is where your problem actually begins. When autofac resolves this instance it does exactly what it is expected to do - injects all the dependencies into TenantIdentitifcationStrategy's constructor including IHttpContextAccessor. So, right there in the constructor you grab an instance of IHttpContext from that context accessor and store it for using in tenant resolution process - and this is a fatal mistake: there's no http request at this time, and since TenantIdentitifcationStrategy is a singleton it means that there will not ever be one for it! So, it gets null request context for the whole application lifespan. This effectively means that TenantIdentitifcationStrategy will not be able to resolve tenant identifier based on http requests - because it does not actually analyze them. Consequently, MultitenantContainer will not be able to resolve any tenant-specific services.

Now when the problem is clear, its solution is obvious and trivial - just move fetching of request context context = _httpContextAccessor.HttpContext to TryIdentifyTenant() method. It gets called in the proper context and will be able to access request context and analyze it.

PS. This digging has been highly educational for me since I had absolutely no idea about autofac's multi-tenant concept, so thank you very much for such an interesting question! :)

PPS. And one more thing: this question is just a perfect example of how important well prepared example is. You provided very good example. Without it no one would be able to figure out what the problem is since the most important part of it was not presented in the question - and sometimes you just don't know where this part actually is...

1
4/25/2018 12:09:14 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow