SimpleInjector
SimpleInjector copied to clipboard
Multi-Tenant IoC Strategy with ASP.NET Core 6
I'm interested in your recommendations for implementing a multi-tenancy with Web API and ASP.NET Core 6. Firstly I have followed the tutorial here to get the basics setup and that worked like a charm https://docs.simpleinjector.org/en/latest/aspnetintegration.html.
I then have also been reviewing the suggestions you have for .NET Framework here - https://github.com/simpleinjector/SimpleInjector/issues/599. This can't work for ASP.NET Core 6 (and earlier) because ASP.NET Core doesn't implement a DependencyResolver.SetResolver or IDependencyResolver.
I've spent some time trying to piece together other posts you've made related to ASP.NET Core and I can't find the appropriate way to hook into the pipeline to achieve the same as your suggestion. Would I need to override SimpleInjectorControllerActivator / IControllerActivator? If I did that I'd then if I wanted to use other pieces of MVC I'd need to override each other Activator as well - e.g. AddPageModelActivation.
I feel like there is something I'm overlooking in the documentation and would love some pointers on the right direction.
Thanks
Hi @IfThingsThenStuff,
From your question I assume that you want to use the one-container-per-tenant solution, while having everything run in a single web application in a single process.
Although this is possible, please keep in mind that there are limitations here while using ASP.NET Core. ASP.NET Core has its own internal DI Container (MS.DI) and that container instance is tightly coupled to the web application instance. The Simple Injector ASP.NET Core integration couples the Simple Injector container to the MS.DI container in order to do cross wiring. This means that there can only be one Simple Injector container instance in your web application that resolves MVC Controllers.
Although I assume that there are technically ways around this, this is not how the Simple Injector ASP.NET Core integration package is set up, which likely means you will have to rewrite part of the integration. But there are other solutions to pursuit, such as:
- Running each tenant in its own Web API with its own port number. With ASP.NET Core this can all still run from within the same process. This article might get you started.
- Running all tenants using one container instance and using one single web api end point/port and register an
ITenantContextabstraction that allows code to choose the tenant based on this injected abstraction. It can be registered as scoped and configured to pull the tenant id from the request, cookie, or other mechanism you provide. - Run all tenants in one single web api end point and implement your Controllers to dispatch to your business layer that runs an isolated container instance per tenant. This likely requires you to implement some sort of Mediator (e.g. MediatR) or commands and queries. This can be done by adding a special
IMediatorabstraction or a special command/query handler decorator that dispatches the request to the correct container instance based on the Tenant ID. There are some caveats, though, because you likely need to integrate Simple Injector with the MS.DI container, which means you should have a container pair per tenant. This page will give you some guidance in doing this. - Or, if you would like to take this to the next level, -building on top of the previous suggestion- you can apply a 'controllerless' Web API model where you skip writing MVC Controllers all together and map incoming URLs directly to underlying handlers in the business layer. This method is described here with an example for ASP.NET Core 6 here. But please note that this is still rough at the edges. For instance, currently that example only supports Web API requests as POST operations. This will be okay for Web API projects that are only consumed from within your own application (e.g. smart phone apps owned by your team), but perhaps less suitable for externally facing Web APIs that are directly used by customers. Also note that you will have to find the correct 'interception' point to dispatch to the right container based on the tenant id. This will probably mean making changes to the Commands and Queries class in such way that a dictionary of containers is injected. It likely also means tinkering with the Swagger definition in such way that the correct documentation is returned that includes information about the Tenant Id, which you likely want to put in the URL.
Please let me know which solution you would like to pursuit and I might be able to help you out down the road if you get stuck.
Good luck
Firstly - thank you for the time and effort you took to write such a detailed response, I am very grateful. When describing my situation I considered the appropriate amount of information to provide, it wasn't obvious what aspects would be most relevant initially, I will provide more detail along with responding to your suggestions.
I have a large mature .NET Framework application that has many Web API endpoints. We started retrofitting Simple Injector as our DI framework 3 years ago. The application is "sort of" multi-tenant. We have single app server clusters with one database per tenant. The tenant information is pulled from a cookie. This means that 2 different tenants can share the same instance of dependencies. It would be nice if we could load specific dependencies per tenant, albeit this is a somewhat theoretical problem it seems to be a helpful design to guarantee segregation of data in some scenarios.
We have many existing Web API controllers that are used internally to the application and we are looking at exposing these to public API's that could be used outside of the application.
Our public API's will be based on the existing controllers but will be written in .NET Core 6. We are also looking at better separation of dependencies through dedicated containers per tenant with each tenant having a dedicated URL. The .NET Framework solution seemed ideal for our scenario, I do understand the explanation provided and it now makes sense why I couldn't find a solution.
Running each tenant in its own Web API with its own port number. With ASP.NET Core this can all still run from within the same process. This article might get you started.
I think this could be problematic from an operations perspective.
Run all tenants in one single web api end point and implement your Controllers to dispatch to your business layer
I'll consider this but I think it could have downsides to putting all api's through a single web api endpoint. Given the fact that we have a number of existing controllers we plan on re-using I don't think this would be ideal.
Running all tenants using one container instance and using one single web api end point/port and register an ITenantContext abstraction that allows code to choose the tenant based on this injected abstraction
This is what we currently do and it is certainly an option - I did like the idea of having separate containers per tenant however.
this is not how the Simple Injector ASP.NET Core integration package is set up, which likely means you will have to rewrite part of the integration
I am interested in at least considering what this would look like. I've been looking at the underlying code implementation for SimpleInjector.Integration.AspNetCore. Am I right in thinking I'd need to provide an alternate version of SimpleInjectorControllerActivator and implement a multi-tenant version of this
private InstanceProducer? GetControllerProducer(Type controllerType) =>
this.container.GetCurrentRegistrations().SingleOrDefault(r => r.ServiceType == controllerType);
We have single app server clusters with one database per tenant. The tenant information is pulled from a cookie. This means that 2 different tenants can share the same instance of dependencies.
It would be nice if we could load specific dependencies per tenant
Assuming you're running in a single process (as by your requirements), you've basically got two options in doing this:
- either you give each tenant its own container instance (as you are considering)
- or you register dispatchers/proxies for abstractions that have multiple tenant implementations.
To demonstrate the second option, consider this IService with a default implementation, and two tenant-specific implementations:
public interface IService { }
[Tenant(1)] public class Tenant1Service : IService { }
[Tenant(2)] public class Tenant2Service : IService { }
public class DefaultService : IService { }
Notice how the tenant-specific implementations are marked with a custom Tenant attribute. This allows the following proxy implementation:
public class TenantSwitchingServiceProxy : IService
{
private readonly ITenantContext context;
private readonly DefaultService defaultService;
private readonly Dictionary<int, DependencyMetadata<IService>> tenantServices;
public TenantSwitchingServiceProxy(
ITenantContext context,
DefaultService defaultService,
IList<DependencyMetadata<IService>> tenantServices)
{
this.context = context;
this.defaultService = defaultService;
// In the context of Simple Injector, it's safe to call ToDictionary on a
// IList<DependencyMetadata<T>> from inside the constructor.
this.tenantServices = tenantServices.ToDictionary(
m => m.ImplementationType.GetCustomAttribute<TenantAttribute>().Id);
}
public void SomeMethod()
{
// Dispatch to correct underlying service.
this.GetService().SomeMethod();
}
private IService GetService() =>
this.tenantServices.TryGetValue(this.context.TenantId, out var meta)
? meta.GetInstance()
: this.defaultService;
}
This proxy is specific to Simple Injector (because of the dependency on Simple Injector's DependencyMetadata<T>) so should preferably be placed inside your Composition Root. The DependencyMetadata<T> acts as a factory for a registration with access to its metadata. In this case you access its ImplementationType property in order to get access to the actual created type. This allows accessing the Tenant attribute on the type.
The proxy's SomeMethod dispatches to one of the tenant-specific implementations and when no implementation exists, it falls back to the default implementation.
This proxy can be registered as follows:
container.Register<DefaultService>();
container.Collection.Register<IService>(
typeof(Tenant1Service),
typeof(Tenant2Service));
container.Register<IService, TenantSwitchingServiceProxy>();
There are many ways to implement and wire proxy implementations, so if you have some different needs, and have a hard time figuring out how to compose it using Simple Injector, let me know. We can figure this out together.
In case you have many different abstractions that might require dispatching to tenant-specific implementations, the solution can be refactored such that the amount of duplicate infrastructure code (the proxy is just infrastructural code) can be reduced to a minimum. This might, however, require the use of Dynamic Interception though.
Note that it seems I forgot to mention a third option, which is to dynamically inject the right type based on the tenant, e.g. something along the lines of:
// WARNING: Don't do this
container.Register<IService>(() =>
{
var contenxt = container.GetInstance<ITenantContext>();
switch (context.TenantId) // Uses the dependency during construction.
{
case 1: container.GetInstance<Tenant1Service>();
case 2: container.GetInstance<Tenant2Service>();
default: container.GetInstance<DefaultService>();
}
});
I, however, purposely left this option out of the list, because it makes object graph composition dependent on runtime data, which is very similar to injecting runtime data into components and results in the same set of complications and should, therefore, be prevented.
This is what we currently do and it is certainly an option - I did like the idea of having separate containers per tenant however. I am interested in at least considering what this would look like. Am I right in thinking I'd need to provide an alternate version of SimpleInjectorControllerActivator and implement a multi-tenant version of this.
At the very least your need a custom ControllerActivator implementation, but keep in mind that the ASP.NET Core Integration not only couples the Simple Injector container to the MS.DI container, but also vise versa. There are two options here:
- You completely decouple the containers (meaning don't call
services.AddSimpleInjector(container)), but that prevents you from easily resolving framework dependencies from within your application components, which will be resolved by the Simple Injector container. If you need few to none framework dependencies, this might be a great option, because it is simple to grasp. - You break the reference from MS.DI back to Simple Injector, because you now have multiple Simple Injector containers. This still allows framework components to be cross-wired and injected into application components but disables certain features.
Here's an example of the second option:
SimpleInjector.Container[] tenantContainers = BuildTenantContainers();
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddControllersWithViews();
foreach (var container in tenantContainers)
{
services.AddSimpleInjector(container, options =>
{
// Must disable disposing, because this couples MS.DI to Simple Injector. Disposing of containers
// must be done manually via another mechanism. (we might be able to simplify this in a future
// release, though).
options.DisposeContainerWithServiceProvider = false;
// WARNING: although calling options.AddAspNetCore() will work and adds the possibility to
// automatically wrap requests in a Simple Injector scope, it does cause a scope to be started
// for EVERY tenant container on EACH request, which might cause overhead in case you have many
// containers. Instead, it's better to manually start a scope from within your custom controller
// activator.
// In case AddAspNetCore() is *not* called, the ServiceProviderAccessor behavior should likely be
// changed to allow tenant containers to resolve scoped instances from the context of the original
// requests. Not setting this, causes any Simple Injector scope to be companioned by a new service
// scope. Note: OnePerRequestServiceProviderAccessor is internal, you should make a copy.
options.ServiceProviderAccessor =
new OnePerRequestServiceProviderAccessor();
});
}
// Important: remove the references back to the container that AddSimpleInjector added, because it will
// just point to one of the containers, which will likely cause MS.DI-resolved components to be injected
// with the container for the wrong tenant. This might, however, cause some parts of the integration
while (null != services.FirstOrDefault(sd => sd.ServiceType == typeof(Container)))
{
services.Remove(services.First(sd => sd.ServiceType == typeof(Container)));
}
// Add your custom controller activator here.
services.AddScoped<IControllerActivator>(
c => new MyCustomControllerActivator(tenantContainers));
WebApplication app = builder.Build();
foreach (var container in tenantContainers)
{
app.Services.UseSimpleInjector(container);
}
As you can see, there are quite some notes, warnings, and caveats to consider. The solution depends on what behavior you require:
- Do your application components depend on framework components that are scoped to the request, or is it okay for them to get a fresh instance (easiest solution). A DbContext for instance, when resolved from MS.DI, is typically fine to live in a scope separate from the web request, but from other MS.DI-resolved components you might want to move their state to other scopes. Can't think of a statefull framework component from the top of my head, but I know they exist.
- Do you require the tenant containers to dispose upon shutdown.
- Do you have many tenant containers or just a few. When you have just a few, it's not a real problem that they all start a scope on each request, if you have dozens... you might start to see an impact on performance.
Knowing that the code provided above is not a complete solution, I hope this still provides you with some clues and guidance that helps you in your research.
A last point I would like to make is that although the Simple Injector integration doesn't make it very easy, the complexity would still exist when working with MS.DI only, because ASP.NET Core is designed to work with one MS.DI container instance. So having a multi-container approach with MS.DI would still cause the same kinds of difficulties as you are experiencing now.
Let me know if you have any follow-up questions.