Search code examples
c#asp.net-coreautofac

asp.net core 3.1 integrate autofac with finbuckle


I'm building a multitenant asp.net core 3.1 app with several database (one for each tenant and one master database). I'm using Autofac for having the Singleton per tenant lifetime support for my services (for now i'm not need the tenant override, i need only the custom SinglePerTenant lifetime) I'm using

My tenant identification strategy involve a db call to the master database.

As written in the documentation of autofac, the identification strategy should not have calls to the database since it is called at each dependency resolution. Then i used another solution for tenant identification (Finbuckle.MultiTenant).

With Finbukle when a request arrives his identification strategy is called (once per htp request), i put the db call in his identification strategy (for optimization i can cache the result, and refresh the query once a day) and a tenantInfo object is set in the HttpContext.

Then In the AutoFac identification strategy i try to read the object setted by FinBuckle, but is not possibile because the Autofac identification Strategy is called before the FinBuckle ones and the desidered property is null.

My Program.cs is:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Startup.cs :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMultiTenant().WithStrategy<TestStategy>(ServiceLifetime.Singleton).WithStore<CustomTestStore>(ServiceLifetime.Singleton); //enable the multitenant support from finbukle

        services.AddControllers();

        services.AddAutofacMultitenantRequestServices();

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.UseMultiTenant() //finbukle

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

    public void ConfigureContainer(ContainerBuilder builder)
    {
         builder.RegisterType<TestDiA>().As<ITestDI>().InstancePerTenant();
    }

    public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
    {
        // This is the MULTITENANT PART. Set up your tenant-specific stuff here.
        var strategy = new MyAutofacTenantIdentificationStrategy(container.Resolve<IHttpContextAccessor>());
        var mtc = new MultitenantContainer(strategy, container);
        return mtc;
    }
}

Autofac tenant identification strategy:

public class MyAutofacTenantIdentificationStrategy : ITenantIdentificationStrategy
{
    private readonly IHttpContextAccessor httpContextAccessor;
    public MyAutofacTenantIdentificationStrategy(
      IHttpContextAccessor httpContextAccessor
    )
    {
        this.httpContextAccessor = httpContextAccessor;
    }
    public bool TryIdentifyTenant(out object tenantId)
    {
        tenantId = null;
        var context = httpContextAccessor.HttpContext;
        if (context == null)
            return false;


        var identifier = context.getTenatInfo()?.Identifier ?? null; //getTenantInfo is a method that extract the tenant info object setted by finbukle
        tenantId = identifier;
        return (tenantId != null || tenantId == (object)"");
    }
}

I'm using Autofac.AspNetCore.Multitenant 3.0.0, Autofac.Extensions.DependencyInjection 6.0.0 and FinBuckle.MultiTenant 5.0.4

I'm really new in this area, so I apologize if I ask a trivial question. There is a way of solving the problem with this approach?

Or there is an alternative strategy for my Issue?


Solution

  • At this time I don't believe Finbuckle and Autofac.Multitenant are compatible.

    Autofac multitenant support for ASP.NET Core relies on running first thing in the middleware pipeline so it can set HttpContext.RequestServices to be based on the tenant scope. As part of that, of course, the tenant identification strategy is going to run.

    However, Finbuckle assumes every tenant is sharing a container like the default ASP.NET Core functionality. Finbuckle middleware tries to use HttpContext.RequestServices to identify the tenant based on registered strategies.

    You can see the sort of chicken/egg problem this raises - request services should be based on the tenant lifetime scope, but the tenant identification strategy requires resolving things from the request services.

    However, let's ignore that for a second because the question was how to avoid the database call for identification on every resolve.

    If you dive into the Finbuckle code, the middleware sets the tenant information on HttpContext.Items as part of their middleware running. Later, when you retrieve the tenant info, it gets retrieved from HttpContext.Items, not re-resolved through the database. The database call executes only once, the first time tenant ID is run.

    That's probably fine. Depending on how many tenants you plan on supporting and how often they change, it might be worth adding some sort of in-memory caching layer you could use to store tenant ID data (whatever is stored in the database that helps you identify that tenant) so you can try the in-memory store first before hitting the database. Maybe it periodically expires data in there or maybe it's a fixed size or whatever... the caching strategy is entirely app-dependent and there's no way I could ever recommend specifics around that. Point being, that's one way to alleviate the database call.

    But getting back to the chicken/egg problem, I don't see an easy way around that.

    If it was me and I had to get this working, I'd probably skip calling the IApplicationBuilder.UseMultiTenant() extension and then create my own version of their middleware which, instead of using HttpContext.RequestServices to get the tenant ID strategies, would take a multitenant container right in the constructor like the Autofac multitenant middleware and would directly use the application-level container to resolve those strategies. That would, of course, have to run before the Autofac multitenant request services middleware, and forcing middleware order is sort of painful. Finally, since the HttpContext.Items would end up having the tenant identification after that middleware runs, your Autofac ITenantIdentificationStrategy could simply look there to get the data and not call the database at all.

    However...

    HUGE, AMAZINGLY IMPORTANT DISCLAIMER

    • I have never in my life used Finbuckle. I can browse the code on GitHub but I don't know what side effects the above consideration might have.
    • I haven't actually tried the above consideration. It may not work.
    • I'm an Autofac project maintainer and wrote the original multitenant support including the original integration with ASP.NET Core. It was very painful to get it to work... so when I say the above consideration might be tricky, I've been there.
    • I very specifically call this a 'consideration' - something to consider - and not a recommendation because I'm not going to necessarily 'recommend' something I don't have a lot of confidence in. I don't have confidence because, again, I don't use Finbuckle.

    MY ACTUAL RECOMMENDATION is... well, to slow down a little. As you mentioned, you're new to this area, and it seems like the stuff you're going to be running into here is pretty deep. I recommend diving into the actual Finbuckle code on GitHub if you haven't. It doesn't look like there's much and it could give you some insight as to what's going on. I recommend trying to create a multitenant app with just Autofac multitenancy, and one with just Finbuckle. See if you really need both. Maybe just one makes sense. For example, it seems like Finbuckle already has multitenancy for data storage; that's what many people use Autofac multitenancy for too - to register different database contexts for each tenant. Perhaps using just one of the products would be enough, and that could remove the whole problem.