Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac.Multitenant in an aspnet core application does not seem to resolve tenant scoped dependencies correctly

I'm in the process of upgrading a Multitenant dotnet core solution which utilises the Autofac.Multitenant framework. I'm not having a lot of luck getting tenancy resolution working correctly. I've created a simple demonstration of the problem here: https://github.com/SaltyDH/AutofacMultitenancy1

This repo demonstrates registering a InstancePerTenant scoped dependency TestMultitenancyContext which is resolved in the Home Controller. Due to issues with using IHttpContextAccessor, I'm using a custom RequestMiddleware class to capture the current HttpContext object so that I can perform logic on the current HttpContext request object in the MultitenantIdentificationStrategy.

Finally, TestFixture provides a simple xUnit test which, at least on my machine returns "tenant1" for both tenants.

Is there something I've missed here or is this just not currently working?

like image 361
Salty Avatar asked Dec 15 '22 04:12

Salty


1 Answers

UPDATE 10/6/2017: We released Autofac.AspNetCore.Multitenant to wrap up the solution to this in a more easy to consume package. I'll leave the original answer/explanation here for posterity, but if you're hitting this you can go grab that package and move on.


I think you're running into a timing issue.

If you pop open the debugger on the HttpContext in the middleware you can see that there's a RequestServicesFeature object on a property called ServiceProvidersFeature. That's what's responsible for creating the per-request scope. The scope gets created the first time it's accessed.

It appears that the order goes roughly like this:

  1. The WebHostBuilder adds a startup filter to enable request services to be added to the pipeline.
  2. The startup filter, AutoRequestServicesStartupFilter, adds middleware to the very beginning of the pipeline to trigger the creation of request services.
  3. The middleware that gets added, RequestServicesContainerMiddleware, basically just invokes the RequestServices property from the ServiceProvidersFeature to trigger creation of the per-request lifetime scope. However, in its constructor is where it gets the IServiceScopeFactory that it uses to create the request scope, which isn't so great because it'll be created from the root container before a tenant can be established.

All that yields a situation where the per-request scope has already been determined to be for the default tenant and you can't really change it.

To work around this, you need to set up request services yourself such that they account for multitenancy.

It sounds worse than it is.

First, we need a reference to the application container. We need the ability to resolve something from application-level services rather than request services. I did that by adding a static property to your Startup class and keeping the container there.

public static IContainer ApplicationContainer { get; private set; }

Next, we're going to change your middleware to look more like the RequestServicesContainerMiddleware. You need to set the HttpContext first so your tenant ID strategy works. After that, you can get an IServiceScopeFactory and follow the same pattern they do in RequestServicesContainerMiddleware.

public class RequestMiddleware
{
  private static readonly AsyncLocal<HttpContext> _context = new AsyncLocal<HttpContext>();

  private readonly RequestDelegate _next;

  public RequestMiddleware(RequestDelegate next)
  {
    this._next = next;
  }

  public static HttpContext Context => _context.Value;

  public async Task Invoke(HttpContext context)
  {
    _context.Value = context;
    var existingFeature = context.Features.Get<IServiceProvidersFeature>();
    using (var feature = new RequestServicesFeature(Startup.ApplicationContainer.Resolve<IServiceScopeFactory>()))
    {
      try
      {
        context.Features.Set<IServiceProvidersFeature>(feature);
        await this._next.Invoke(context);
      }
      finally
      {
        context.Features.Set(existingFeature);
        _context.Value = null;
      }
    }
  }
}

Now you need a startup filter to get your middleware in there. You need a startup filter because otherwise the RequestServicesContainerMiddleware will run too early in the pipeline and things will already start resolving from the wrong tenant scope.

public class RequestStartupFilter : IStartupFilter
{
  public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
  {
    return builder =>
    {
      builder.UseMiddleware<RequestMiddleware>();
      next(builder);
    };
  }
}

Add the startup filter to the very start of the services collection. You need your startup filter to run before AutoRequestServicesStartupFilter.

The ConfigureServices ends up looking like this:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.Insert(0, new ServiceDescriptor(typeof(IStartupFilter), typeof(RequestStartupFilter), ServiceLifetime.Transient));
  services.AddMvc();

  var builder = new ContainerBuilder();
  builder.RegisterType<TestMultitenancyContext>().InstancePerTenant();

  builder.Populate(services);
  var container = new MultitenantContainer(new MultitenantIdentificationStrategy(), builder.Build());
  ApplicationContainer = container;
  return new AutofacServiceProvider(container);
}

Note the Insert call in there to jam your service registration at the top, before their startup filter.

The new order of operations will be:

  1. At app startup...
    1. Your startup filter will add your custom request services middleware to the pipeline.
    2. The AutoRequestServicesStartupFilter will add the RequestServicesContainerMiddleware to the pipeline.
  2. During a request...
    1. Your custom request middleware will set up request services based on the inbound request information.
    2. The RequestServicesContainerMiddleware will see that request services are already set up and will do nothing.
    3. When services are resolved, the request service scope will already be the tenant scope as set up by your custom request middleware and the correct thing will show up.

I tested this locally by switching the tenant ID to come from querystring rather than host name (so I didn't have to set up hosts file entries and all that jazz) and I was able to switch tenant by switching querystring parameters.

Now, you may be able to simplify this a bit. For example, you may be able to get away without a startup filter by doing something directly to the web host builder in the Program class. You may be able to register your startup filter right with the ContainerBuilder before calling builder.Populate and skip that Insert call. You may be able to store the IServiceProvider in the Startup class property if you don't like having Autofac spread through the system. You may be able to get away without a static container property if you create the middleware instance and pass the container in as a constructor parameter yourself. Unfortunately, I already spent a loooot of time trying to figure out the workaround so I'm going to have to leave "optimize it" as an exercise for the reader.

Again, sorry this wasn't clear. I've filed an issue on your behalf to get the docs updated and maybe figure out a better way to do this that's a little more straightforward.

like image 192
Travis Illig Avatar answered Jan 11 '23 23:01

Travis Illig