If you’re not already familiar with Dependency Injection in ASP.NET Core there’s a great write up on it here.
The TLDR is that dependency injection is now built into the framework so we can wave goodbye to the various different flavours of service locator we inevitably ended up having to use in existing ASP.NET frameworks. The new architecture makes it easier to build loosely coupled applications that are easy to extend and test.
Dependency Scopes
A dependency scope indicates the scope of an instance created by a DI tool. The most common scopes are:
- Singleton - Instance created once per application
- Request - Instance created once per HTTP Request
- Transient - Instance created each time one is requested
These three scopes are supported OOTB and in most cases are all you will need.
This is true even for multi-tenant applications. Suppose you’re using Entity Framework and each tenant has their own database. Since the recommended pattern is to create a DbContext
instance per request, this will work fine in multi-tenant environments since we resolve the tenant per request. See my previous post for an example of this.
The missing scope - Tenant-Singleton
If a singleton is created once per application, you can probably guess that a tenant-singleton is created once per tenant.
So when might you need this scope? Think of any object that is expensive to create or needs to maintain state yet should be isolated for each tenant. Good examples would be NHibernate’s Session Factory, RavenDB’s Document Store or ASP.NET’s Memory Cache.
First attempt - IServiceScopeFactory
I’m not going to go into a deep overview of the built-in DI - this article does a very good job of that.
Essentially IServiceScopeFactory
is the interface responsible for creating IServiceScope
instances which are in turn responsible for managing the lifetime of IServiceProvider
- which is the interface we use to resolve dependencies i.e. IServiceProvider.GetService(type)
.
In ASP.NET Core, a IServiceScope
is created per request to handle request-scoped dependencies. I figured I just needed to change how this was created, specifically:
- Get the tenant service scope
- If the tenant service scope doesn’t exist (first request to tenant), create and configure it
- Create a child scope from the tenant scope for the request
These three steps rely on three important features available in most popular DI tools:
- The ability to create child containers
- The ability to configure containers at runtime
- Resolving dependencies from a parent container if not explicitly configured on the child (bubbling up)
Unfortunately the built-in IServiceProvider
does not support any of these features. For more “advanced” scenarios the ASP.NET team advocate using another DI tool (my thoughts on that later).
StructureMap is my favourite DI tool and since it supports .NET Core, it was relatively straightforward to create a IServiceScopeFactory
implementation that handled the above requirements. In the SM world this meant:
- Obtaining the tenant container
- If the tenant container doesn’t exist create a child container from the root application container and configure it
- Create a nested request container
The code for doing that:
public virtual IServiceScope CreateScope()
{
var tenantContext = GetTenantContext(Container.GetInstance<IHttpContextAccessor>().HttpContext);
var tenantProfileName = GetTenantProfileName(tenantContext);
var tenantProfileContainer = Container.GetProfile(tenantProfileName);
ConfigureTenantProfileContainer(tenantProfileContainer, tenantContext);
return new StructureMapServiceScope(tenantProfileContainer.GetNestedContainer());
}
With my SM implementation plugged in I was ready to give my sample app a whirl - which much to my dismay, blew up!
Tenant who?
A bit of an oversight on my part was understanding how and when the service scope factory was invoked. It happens at the very start of the request pipeline, before SaasKit’s tenant resolution moddleware kicks in. This meant that the service scope factory was unable to access the current tenant. Hmm…
Whilst it is possible to push the tenant resolution middleware further up the pipeline I didn’t really like the idea that to work correctly SaasKit would be dependent on the order of middleware, since we have little control over this in consuming apps.
Orchard to the rescue
Sebastien Ros is one of the developers on the Orchard Team and also happens to be a regular participant in the SaasKit gitter room.
He told me how they were handling this scenario in Orchard - using middleware to replace the service provider created per request (HttpContext.RequestServices
).
Long story short, I was able to achieve something similar with StructureMap. Here’s the invoke method from the relevant middleware:
public async Task Invoke(HttpContext context, Lazy<ITenantContainerBuilder<TTenant>> builder)
{
Ensure.Argument.NotNull(context, nameof(context));
var tenantContext = context.GetTenantContext<TTenant>();
if (tenantContext != null)
{
var tenantContainer = await GetTenantContainerAsync(tenantContext, builder);
using (var requestContainer = tenantContainer.GetNestedContainer())
{
// Replace the request IServiceProvider created by IServiceScopeFactory
context.RequestServices = requestContainer.GetInstance<IServiceProvider>();
await next.Invoke(context);
}
}
}
With a little syntactic sugar it’s possible to configure tenant-scoped singletons:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMultitenancy<AppTenant, AppTenantResolver>();
var container = new Container();
container.Populate(services);
container.Configure(c =>
{
// Application Services
});
container.ConfigureTenants<AppTenant>(c =>
{
// Tenant Scoped Services
c.For<IMessageService>().Singleton().Use<MessageService>();
});
return container.GetInstance<IServiceProvider>();
}
We’re using the StructureMap.Dnx package which integrates StructureMap with ASP.NET Core.
In the above code I’m registering MessageService
as a singleton in the tenant container. The request container is a nested container created from the tenant container. When an instance of IMessageService
is requested it will be resolved from the tenant container. You can read more on the behaviour of nested containers in the StructureMap Docs.
The final step is to register the middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMultitenancy<AppTenant>();
app.UseTenantContainers<AppTenant>(); // <-- this one
}
Overriding default dependencies
Configuring tenant dependencies in this way makes it possible to override default dependencies per tenant.
The ConfigureTenants
method has an overload that gives you access to the current tenant, making it possible to do things like this:
container.ConfigureTenants<AppTenant>((tenant, config) =>
{
if (tenant.DbType == DbType.RavenDB)
{
config.For<IRepository>().Use<RavenDbRepository>();
}
else
{
config.For<IRepository>().Use<SqlServerRepository>();
}
});
You could also register your default dependencies in the root container and then override them for specific tenants:
container.Configure(c =>
{
c.For<IBackupRepository>().Use<AzureBackupRepository>();
});
container.ConfigureTenants<AppTenant>((tenant, c) =>
{
if (tenant.Name == "Contoso")
{
c.For<IBackupRepository>().Use<AmazonBackupRepository>();
}
});
If your tenant dependency configuration is quite large and you don’t want all of this code sitting in Startup.cs
you can instead create a class that implements ITenantContainerBuilder<T>
:
public interface ITenantContainerBuilder<TTenant>
{
Task<IContainer> BuildAsync(TTenant tenant);
}
For example:
public class AppTenantContainerBuilder : ITenantContainerBuilder<AppTenant>
{
private IContainer container;
public AppTenantContainerBuilder(IContainer container)
{
this.container = container;
}
public Task<IContainer> BuildAsync(AppTenant tenant)
{
var tenantContainer = container.CreateChildContainer();
tenantContainer.Configure(config =>
{
if (tenant.Name == "Tenant 1")
{
config.ForSingletonOf<IMessageService>().Use<OtherMessageService>();
}
else
{
config.ForSingletonOf<IMessageService>().Use<MessageService>();
}
});
return Task.FromResult(tenantContainer);
}
}
Ready to try it?
Add the SaasKit.Multitenancy.StructureMap
package to your project.json or find it on NuGet.
Final thoughts on DI in ASP.NET Core
I’m a little disappointed I couldn’t get this working with the built-in DI. Even the final solution feels like a bit of a hack.
The 3 most common DI tools in .NET are StructureMap, Autofac and Ninject. All three support child containers and runtime configuration. It seems like an oversight to not include these features in the built-in DI or at the very least make them opt-in. Having a response of “use another DI tool” only works if you provide the necessary extensibility points to make use of their “advanced” features.
On another note, I was thinking about the above implementation and how this could be adapted to support Autofac and Ninject:
internal class MultitenantContainerMiddleware<TContainer, TTenant>
{
private readonly RequestDelegate next;
public MultitenantContainerMiddleware(RequestDelegate next)
{
Ensure.Argument.NotNull(next, nameof(next));
this.next = next;
}
public async Task Invoke(HttpContext context, Lazy<ITenantContainerBuilder<TContainer, TTenant>> builder)
{
Ensure.Argument.NotNull(context, nameof(context));
var tenantContext = context.GetTenantContext<TTenant>();
if (tenantContext != null)
{
var tenantContainer = await GetTenantContainerAsync(tenantContext, builder);
using (var requestContainer = tenantContainer.GetNestedContainer())
{
// Replace the request IServiceProvider created by IServiceScopeFactory
context.RequestServices = requestContainer.GetInstance<IServiceProvider>();
await next.Invoke(context);
}
}
}
private async Task<TContainer> GetTenantContainerAsync(
TenantContext<TTenant> tenantContext,
Lazy<ITenantContainerBuilder<TContainer, TTenant>> builder)
{
var tenantContainer = tenantContext.GetTenantContainer<TContainer>();
if (tenantContainer == null)
{
tenantContainer = await builder.Value.BuildAsync(tenantContext.Tenant);
tenantContext.SetTenantContainer(tenantContainer);
}
return tenantContainer;
}
}
We could use this middleware for most DI tools and make it part of the core SaasKit project. Each DI tool would just provide an implementation of ITenantContainerBuilder
.
The only code that still needs to be abstracted is:
using (var requestContainer = tenantContainer.GetNestedContainer())
{
// Replace the request IServiceProvider created by IServiceScopeFactory
context.RequestServices = requestContainer.GetInstance<IServiceProvider>();
await next.Invoke(context);
}
So we could extend ITenantContainerBuilder
to include methods for creating a child container or IServiceProvider
instance.
But then we’ve just created our own DI system and thrown away the built-in one.
1 step forward, 2 steps back.
Download
Get the sample here.