How to implement Multi-tenancy with autofac in asp.net core 3.0
In my past experience of developing enterprise software (which is 12 years ago), the way we implement multi-tenancy was by splitting the data per database. Once our customers reached hundred, the servers to host these databases for each client required high resources and high cost. These infrastructure cost was then bear by the customer as part of the selling price. Is this a sustainable solution for long term? I doubt so.
I have started looking at the multi-tenancy topic since asp.net core 1.0. And recently Microsoft launch asp.net core 3.0 with some breaking changes include the way how to implement it in Autofac.
Hence, I think it would be great for me to share some of my thought around .net core, autofac and multi-tenancy with everyone.
In this example, I will be using WebAPI solution. To create the WebAPI, you could run it from the console dotnet new webapi --name WebApi
To enable to multi-tenancy in Autofac, you will need to install the following packages from nuget:
dotnet add WebAPI package Autofac --version 4.9.4
dotnet add WebAPI package Autofac.AspNetCore.Multitenant --version 2.0.0
dotnet add WebApi package Autofac.Extensions.DependencyInjection --version 5.0.0
Add autofac in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAutofac();
}
I am defining the model for Tenant as follows:
public class Tenant
{
public string TenantCode { get; set; }
public string HostName { get; set; }
public string ConnectionString { get; set; }
}
There are several strategy to identify the tenant. It can be identified by HTTP header, host name or query string. In this example, I will be using the host name as the resolver strategy.
public class TenantResolverStrategy : ITenantIdentificationStrategy
{
private readonly IHttpContextAccessor httpContextAccessor; public TenantResolverStrategy(
IHttpContextAccessor httpContextAccessor
) {
this.httpContextAccessor = httpContextAccessor;
} public bool TryIdentifyTenant(out object tenantId)
{
tenantId = null;
var context = httpContextAccessor.HttpContext;
if (context == null)
return false; var hostName = context?.Request?.Host.Value;
tenantId = hostName; return (tenantId != null || tenantId == (object)"");
}
}
Beside the strategy, we also need to resolve the Tenant instance itself based on the matching host name. The resolved tenant contains connection string which can be different database, different server or same database but different schema.
public class TenantResolver : ITenantResolver
{
private readonly ITenantIdentificationStrategy tenantIdentificationStrategy;
private readonly IMemoryCache memoryCache;
private readonly ITenantService tenantService; public TenantResolver(
ITenantIdentificationStrategy tenantIdentificationStrategy,
IMemoryCache memoryCache,
ITenantService tenantService
) {
this.tenantIdentificationStrategy = tenantIdentificationStrategy;
this.memoryCache = memoryCache;
this.tenantService = tenantService;
} public async Task<Tenant> ResolveAsync(object tenantId)
{
Tenant tenant;
var hostName = (string)tenantId;
if (memoryCache.TryGetValue(hostName, out object cached))
{
tenant = (Tenant)cached;
}
else
{
tenant = await tenantService.GetTenantByHostNameAsync(hostName);
}
return tenant ?? new Tenant();
}
}
The tenantService
is varied depend on how would you implement it. In my context, the tenantService
is querying to the tenant table in the database based on the first found matchinghostName
.
Back to the Startup.cs
file, I add the ConfigureContainer
to setup all the tenant resolvers related stuff.
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<TenantResolver>().As<ITenantResolver>().SingleInstance();
builder.RegisterType<TenantResolverStrategy>().As<ITenantIdentificationStrategy>().SingleInstance(); builder.Register<Tenant>(container =>
{
ITenantIdentificationStrategy strategy = container.Resolve<ITenantIdentificationStrategy>(); strategy.TryIdentifyTenant(out object id); if (id != null)
{
if (container.IsRegistered(typeof(ITenantResolver)))
{
var tenantResolver = container.Resolve<ITenantResolver>();
return tenantResolver.ResolveAsync(id).Result;
}
}
return new Tenant();
}).InstancePerLifetimeScope();
}
In multi-tenancy use cases, it is very common to have a bespoke or specific behaviour which only applies to specific customer. In this case, we could override the dependency injection instance during ConfigureMultitenantContainer
.
public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
{
var strategy = container.Resolve<ITenantIdentificationStrategy>();
var multitenantContainer = new MultitenantContainer(strategy, container);
multitenantContainer.ConfigureTenant("Tenant01", containerBuilder =>
{
containerBuilder.RegisterType<DepartmentServiceTenant01>().As<IDepartmentService>();
});
return multitenantContainer;
}
In above example, we are overriding the IDepartmentService
for Tenant01. It means, Tenant01 will get the instance of DepartmentServiceTenant01
instead of the one injected globally, which is DepartmentService
.
In the Program.cs
, below implementation is only applicable for asp.net core 3.0:
public static void Main(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
.ConfigureWebHostDefaults(webHostBuilder => {
webHostBuilder
.UseConfiguration(configuration)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>();
});
}
All good to go now.
git commit -m ':sparkles: enable multi-tenancy with autofac'
Let me know if you have implement it differently in your past experience or facing some obstacle during the setup.
Cheers 🎉