A multitenant web application is one that responds differently depending on how it is addressed – the tenant. This kind of architecture has become very popular, because a single code base and deployment can serve many different tenants. In this post, I will present some of the concepts and challenges behind multitenant ASP.NET Core apps. Let’s consider what it takes to write a multitenant ASP.NET Core app. For the sake of simplicity, let’s consider two imaginary tenants, ABC and XYZ. We won’t go into all that is involved in writing a multitenant app, but we will get a glimpse of all the relevant stuff that is involved in it.
A tenant has a specific identity, and an application that responds to a particular tenant behaves differently from another tenant. Specifically, one or more of these may change:
By UI I mean a tenant may have different CSS files, different logo images, and so on. Data should be easy to understand – we don’t want tenant ABC to display data for XYZ, and vice-versa. Changes in behavior or functionality are also possible, when a particular tenant has a different feature set than others. For the sake of simplicity, let’s say that a tenant is identified by a string, like ABC or XYZ; this will be its code name.
We will define an interface, ITenantService, that will serve as the entry point for the multi-tenant functionality:
public interface ITenantService { string GetCurrentTenant(); }
And an implementation of it:
public sealed class TenantService : ITenantService { private readonly HttpContext _httpContext; private readonly ITenantIdentificationService _service; public TenantService(IHttpContextAccessor accessor, ITenantIdentificationService service) { this._httpContext = accessor.HttpContext; this._service = service; } public string GetCurrentTenant() { return this._service.GetCurrentTenant(this._httpContext); } }
As you can see, this is very simple – essentially it consists of a method GetCurrentTenant. The actual complexity goes in the implementation strategies. For most of your application-specific code, this is likely the only reference you will need.
First, let’s agree on some basic concepts:
How can we make the application know how it should behave, that is, what tenant should it be serving? For that we need to consider a tenant identification (or resolution) strategy. One can think of several ones, but I’m going to present just three:
We will need to have a default tenant, that is, one that will be inferred if no information is passed by the browser to distinguish it.
Let’s define an interface that can provide us with this information; we’ll call it ITenantIdentificationService, and we already referenced it in the previous snippet:
public class TenantMapping { public string Default { get; set; } public Dictionary<string, string> Tenants { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); }
As you can see, it basically consists of a dictionary of keys and values where each value represents a tenant code, and each key is either a host header or a query string parameter value. We can load an instance of this settings class from the configuration object:
public static class ConfigurationExtensions { public static TenantMapping GetTenantMapping(this IConfiguration configuration) { return configuration.GetSection("Tenants").Get(); } }
The Get<T> extension method comes from the Microsoft.Extensions.Configuration.Binder NuGet package, and it is used to turn configuration into strongly-typed Plain Old CLR Object (POCO) objects. The actual configuration values will depend on the implementations of the identification/resolution service.
As we said, we will have three implementations of ITenantIdentificationService, one using the host header:
public sealed class HostTenantIdentificationService : ITenantIdentificationService { private readonly TenantMapping _tenants; public HostTenantIdentificationService(IConfiguration configuration) { this._tenants = configuration.GetTenantMapping(); } public HostTenantIdentificationService(TenantMapping tenants) { this._tenants = tenants; } public IEnumerable<string> GetAllTenants() { return this._tenants.Tenants.Values; } public string GetCurrentTenant(HttpContext context) { if (!this._tenants.Tenants.TryGetValue(context.Request.Host.Host, out var tenant)) { tenant = this._tenants.Default; } return tenant; } }
The key here is the domain name passed as the host header (eg, abc.com) and the value the tenant code (abc). This allows having many domains pointing to the same tenant, if we want that.
A sample configuration:
{ "Tenants": { "default": "abc", "tenants": { "abc.com": "abc", "xyz.net": "xyz", "127.0.0.1": "xyz" } } }
The other strategy that uses the query string is similar:
public sealed class QueryStringTenantIdentificationService : ITenantIdentificationService { private readonly TenantMapping _tenants; public QueryStringTenantIdentificationService(IConfiguration configuration) { this._tenants = configuration.GetTenantMapping(); } public string GetCurrentTenant(HttpContext context) { var tenant = context.Request.Query["Tenant"].ToString(); if (string.IsNullOrWhiteSpace(tenant) || !this._tenants.Tenants.Values.Contains(tenant, StringComparer.InvariantCultureIgnoreCase)) { return this._tenants.Default; } if (this._tenants.Tenants.TryGetValue(tenant, out var mappedTenant)) { return mappedTenant; } return tenant; } }
The configuration in this case could be:
{ "Tenants": { "default": "abc", "tenants": { "abc": "abc", "xyz": "xyz" } } }
Here, they key in the tenants collection will be the value passed in the query string, for the Tenant parameter (eg, Tenant=abc).
Finally, using the source IP for the request:
{ "Tenants": { "default": "abc", "tenants": { "192.168.1": "abc", "127": "xyz" } } }
So, all requests coming from IPs 192.168.1.* will get tenant abc and all coming from the localhost will get xyz.
Both these implementations are stateless and cause no side effects, they just return whatever they think the current tenant is, from the current HttpContext. These are infrastructure classes, meaning, you should never have to know or reference them. But you do have to register the right one on the dependency injection (DI) framework of ASP.NET Core, it’s as easy as:
services.AddSingleton<ITenantIdentificationService, HostTenantIdentificationService>();
And we will also need to configure the mappings between host names (or query string values) and the default tenant, in the appsettings.json file, with values appropriate to the resolution strategy in use.
When it comes to the user interface, we may want to do different things:
For the first case, we will make use of a tag helper. Tag helpers were introduced in ASP.NET Core 2.0 and they are a way to declare components on a Razor view. The TenantTagHelper will show contents or not depending on whether the current tenant matches a list we give it as a parameter. It will look like this:
[HtmlTargetElement("tenant")] public sealed class TenantTagHelper : TagHelper { private readonly ITenantService _service; public TenantTagHelper(ITenantService service) { this._service = service; } [HtmlAttributeName("name")] public string Name { get; set; } public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var tenant = this.Name ?? string.Empty; if (tenant != this._service.GetCurrentTenant()) { output.SuppressOutput(); } return base.ProcessAsync(context, output); } }
The tag helper gets, through constructor dependency injection, the ITenantService instance and uses it to get the current tenant. If it doesn’t match the tenant passed as a parameter, then the output is suppressed.
Like all tag helpers, it needs to be registered before it can be used in a view. The usual location for this is the ViewsShared_ViewImports.cshtml file:
This is content-specific to tenant ABC!
And the content will only show for tenant ABC. You need to keep in mind this approach requires you to explicitly hardcode the tenant’s name. This may or not be ideal for you.
If we want to serve different files, it’s a whole different thing. As you may know, ASP.NET Core relies on some conventions for looking up where to find markup files (.cshtml). These are generally located under Views<controller>, but we can override this by providing our own view location expander. A view location expander implements IViewLocationExpander (who could tell?) and needs to be registered for the Razor view engine options, upon startup:
public sealed class TenantViewLocationExpander : IViewLocationExpander { private ITenantService _service; private string _tenant; public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) { foreach (var location in viewLocations) { yield return location.Replace("{0}", this._tenant + "/{0}"); yield return location; } } public void PopulateValues(ViewLocationExpanderContext context) { this._service = context.ActionContext.HttpContext.RequestServices.GetService(); this._tenant = this._service.GetCurrentTenant(); } }
The view locations passed to ExpandViewLocations are:
Where {0} is the view and {1} the controller name. What we are doing here is first returning a version with the tenant prepended to it, e.g.:
Before all the others, this makes sure that if a folder exists with the tenant’s name, any files will be loaded from there. Again, the code receives on its constructor the notorious ITenantService, used to get the current tenant, and, in the PopulateValues, passes the tenant along to the ExpandViewLocations. This is the method that is responsible for returning the physical locations where the view files (.cshtml) are to be found. We are just returning the current locations but, for each registered location, another one which includes the current tenant’s code. This way, we ensure that, if the file is found, it is used before. To be more precise, it allows us to have this:
So, for controller Home, the folder abc will be searched when the application tries to locate static files for the abc tenant. So, when your HomeController‘s Index action method returns a call to View, the Index.cshtml file will be retrieved from the ViewsHomeabc folder.
When it comes to configuration, it is somewhat tricky to set different values per tenant, especially because at application startup we do not know the current tenant, because it only exists on the scope of a request. But there are a couple of things we can do.
We can have different named configuration values associated with the same POCO class. In the Startup class’ ConfigureServices method, add code like this:
services.Configure("abc", options => { options.NumberOption = 1; options.StringOption = "abc"; }); services.Configure("xyz", options => { options.NumberOption = 2; options.StringOption = "xyz"; });
Again, I am hardcoding values for each tenant (abc and xyz), do keep this in mind. Forget about the PerTenantSettings class, this is just some class that can be used to pass arbitrary parameters to the different tenants and we won’t cover it here. Now, if we inject a configuration into any component, such as a controller, we can do it like this:
public HomeController(IOptionsSnapshot settings, ITenantService service) { var tenant = service.GetCurrentTenant(); var tenantSettings = settings.Get(tenant); }
This relies on the IOptionsSnapshot<T>‘s ability to retrieve named configuration entries. This interface and associated capability comes from the Microsoft.Extensions.Options NuGet package. The name you pass it must be one that was also set when calling Configure<T>, as we saw earlier.
What if we want to have different registrations per tenant on the service provider? This is a bit more complex, but let’s see a way by which we can accomplish it. First, we declare an interface that represents this capacity:
public interface ITenantConfiguration { void Configure(IConfiguration configuration); void ConfigureServices(IServiceCollection services); }
and a particular implementation:
public sealed class abcTenantConfiguration : ITenantConfiguration { public void Configure(IConfiguration configuration) { configuration["StringOption"] = "abc"; } public void ConfigureServices(IServiceCollection services) { services.AddScoped<IMyService, XptoService>(); } public string Tenant => "abc"; }
As you can see, we can both override configuration settings for the current tenant and provide alternative service implementations for registered services. Now we need a way to load this, and we need to do it when configuring the registered services, usually in the ConfigureServices method of the Startup class. Note that we cannot use logic for the different tenants in ConfigureServices, because by the time it is called, there is not request yet, and therefore we do not know what the current tenant is. Instead, we shall create a couple of clever extension methods just for this purpose:
public static class ServiceCollectionExtensions { public static IServiceCollection AddTenantConfiguration(this IServiceCollection services, Assembly assembly) { var types = assembly .GetExportedTypes() .Where(type => typeof(ITenantConfiguration).IsAssignableFrom(type)) .Where(type => (type.IsAbstract == false) && (type.IsInterface == false)); services.AddScoped(typeof(ITenantConfiguration), sp => { var svc = sp.GetRequiredService<ITenantService>(); var configuration = sp.GetRequiredService<IConfiguration>(); var tenant = svc.GetCurrentTenant(); var instance = types .Select(type => ActivatorUtilities.CreateInstance(sp, type)) .OfType<ITenantConfiguration>() .SingleOrDefault(x => x.Tenant == tenant); if (instance != null) { instance.Configure(configuration); instance.ConfigureServices(services); sp.GetRequiredService<IHttpContextAccessor>().HttpContext.RequestServices = services.BuildServiceProvider(); return instance; } else { return DummyTenantServiceProviderConfiguration.Instance; } }); return services; } public static IServiceCollection AddTenantConfiguration<T>(this IServiceCollection services) { var assembly = typeof(T).Assembly; return services.AddTenantConfiguration(assembly); } }
public sealed class DynamicTenantIdentificationService : ITenantIdentificationService { private readonly Func<HttpContext, string> _currentTenant; private readonly Func<IEnumerable<string>> _allTenants; public DynamicTenantIdentificationService(Func<HttpContext, string> currentTenant, Func<IEnumerable<string>> allTenants) { if (currentTenant == null) { throw new ArgumentNullException(nameof(currentTenant)); } if (allTenants == null) { throw new ArgumentNullException(nameof(allTenants)); } this._currentTenant = currentTenant; this._allTenants = allTenants; } public IEnumerable<string> GetAllTenants() { return this._allTenants(); } public string GetCurrentTenant(HttpContext context) { return this._currentTenant(context); } }
So, we now have a way for a specific tenant to override the registered services or some configuration values at will! All we have to do is provide an instance of a class implementing ITenantConfiguration, pretty much like abcTenantConfiguration shown above. To use this, just call one of the extension methods in the Startup class:
services.AddTenantConfiguration();
Another option that I leave as an exercise to you would be to use Managed Extensibility Framework (MEF) or any other similar framework to dynamically load.
What if you need to access configuration values from views? Let’s consider we will have a configuration section for each tenant in the configuration file (appsettings.json), something like this:
{ "Tenants": { "abc": { "StringOption": "abc", "NumberOption": 1 }, "xyz": { "StringOption": "xyz", "NumberOption": 2 } } }
We need to retrieve the configuration information relative to the current tenant, this code does just that:
public static class RazorPageExtensions { public static T GetValueForTenant(this IRazorPage page, string setting, T defaultValue = default(T)) { var service = page.ViewContext.HttpContext.RequestServices.GetService(); var tenant = service.GetCurrentTenant(); var configuration = page.ViewContext.HttpContext.RequestServices.GetService(); var section = configuration.GetSection("Tenants").GetSection(tenant); if (section.Exists()) { return section.GetValue(setting, defaultValue); } else { return configuration.GetValue(setting, defaultValue); } } }
If the section or the named configuration setting does not exist, the default value will be returned instead.
From a Razor view, we can now do:
String Option: @this.GetValueForTenant("StringOption")
When it comes to retrieving different values from a relational database, we have essentially three options:
All of these have their pros and cons, for example:
For the purpose of this article, we will stick to Entity Framework Core, and therefore we will be using a DbContext to retrieve the data – for that, you will need the Microsoft.EntityFrameworkCore NuGet package and also the one that contains the SQL Server implementation (Microsoft.EntityFrameworkCore.SqlServer). Again, let’s define an interface that represents this functionality – setting database parameters depending on the tenant:
public abstract class TenantContext : DbContext { protected TenantContext(DbContextOptions options) : base(options) { } private TenantContext() { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var svc = this.GetService(); svc.OnModelCreating(modelBuilder, this); } public override int SaveChanges() { var svc = this.GetService(); svc.SaveChanges(this); return base.SaveChanges(); } // rest goes here }
This class is meant to serve as a basis for any application-specific DbContext as it contains the basic blocks to make it work in a multitenant way:
You see that OnModelCreating gets the registered ITenantDbContext (whatever it is) from the service provider and calls its OnModelCreating method. Then, when the context is saving entities, it calls SaveChanges on the service reference. This context class needs to be registered with the service provider too:
services.AddDbContext(options => { options.UseSqlServer(this.Configuration.GetConnectionString("DefaultConnection")); });
Without this, the ITenantDbContext would not be injectable into the context. Let’s see the possible implementations of this interface, for each of the three discussed strategies.
Here goes the implementation for the different schemas strategy:
public interface ITenantEntity { }
Just a marker entity, as you can see. For each of these entities, it sets the schema property to be the current tenant, as returned by the injected ITenantService. SaveChanges does nothing, as there is no need to modify the entities when they are saved. Not too complex, I’d say.
For the different databases strategy we will create a DifferentConnectionTenantDbContext class:
{ "ConnectionStrings": { "abc": "", "xyz": "" } }
The last one is somewhat trickier: we need to leverage a couple of features that are available in the latest versions of Entity Framework Core, such as shadow properties and global query filters. Without further ado, here is the implementation:
public sealed class FilterTenantDbContext : ITenantDbContext { private readonly ITenantService _service; private static readonly MethodInfo _propertyMethod = typeof(EF).GetMethod(nameof(EF.Property), BindingFlags.Static | BindingFlags.Public).MakeGenericMethod(typeof(string)); private LambdaExpression IsTenantRestriction(Type type, string tenant) { var parm = Expression.Parameter(type, "it"); var prop = Expression.Call(_propertyMethod, parm, Expression.Constant("Tenant")); var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(tenant)); var lambda = Expression.Lambda(condition, parm); return lambda; } public FilterTenantDbContext(ITenantService service) { this._service = service; } public void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { var tenant = this._service.GetCurrentTenant(); foreach (var entity in modelBuilder.Model.GetEntityTypes().Where(x => typeof(ITenantEntity).IsAssignableFrom(x.ClrType))) { entity.AddProperty("Tenant", typeof(string)); modelBuilder .Entity(entity.ClrType) .HasQueryFilter(this.IsTenantRestriction(entity.ClrType, tenant)); } } public void SaveChanges(DbContext context) { var svc = context.GetService(); var tenant = svc.GetCurrentTenant(); foreach (var entity in context.ChangeTracker.Entries().Where(e => e.State == EntityState.Added)) { entity.Property(nameof(TenantService.Tenant)).CurrentValue = tenant; } } }
re A shadow property is one that does not exist in the POCO model but exists on the database. This is useful because we cannot easily query it – we probably won’t even know that it exists, as it doesn’t show up as a property in our class. A global query filter is a restriction that is automatically applied to all queries over a given type. This is most useful for implementing soft deletes and, you got it, multitenant apps! In OnModelCreating we first list all entities in the model that implement ITenantEntity – the marker interface used to tell those entities that need to be made multitenant-aware -, then we add a shadow property Tenant of type string to them – this will be used to filter by the current tenant. Lastly, we add a global filter in the form of a LINQ expression that automatically filters all accesses to multitenant entities by the current tenant code, as returned by ITenantService. Having an entity implement ITenantEntity is as easy as adding its declaration, no need to add any members:
services.AddSingleton<ITenantDbContext, DifferentConnectionTenantDbContext>();
Do not forget about this, or you will get an exception on the context’s OnModelCreating method. If you want, you can always have a dummy (Null Object Pattern implementation):
public sealed class DummyTenantDbContext : ITenantDbContext { public void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { } public void SaveChanges(DbContext context) { } }
When it comes to having different behavior, you should have service classes that return values that depend on the current tenant, and therefore can be used to make decisions. We saw how we can change the configuration, data access strategy or even service implementations based on the current tenant. You need to leverage these techniques to suit what you want to accomplish. For example, say you have a IDecisionService service injected into your controller:
[HttpPost] public IActionResult Post(string option) { var view = this._decisionService.SelectView(option); return this.View(view); }
The actual IDecisionService implementation can probably receive a multitenant-aware DbContext or an ITenantService instance, but not likely one of the other infrastructure classes. It can then use these to make informed decisions of what to do. The possibilities are endless, but make sure you design your application with extensibility in mind, that is, not hardcoding it to specific tenants or “magic” values.
Other topics might include:
However, I won’t go through these right now, as they can get quite complex. Maybe something for another article!
We’ve seen the basic building blocks for a multitenant architecture and also some reference implementations.
A lot more can be said, but I believe this will get you up to speed with this kind of architecture. For your convenience, I am listing here the configuration steps that must be followed, again, all should go in the ConfigureServices method of the Startup class.
// for having different Razor .cshtml files per tenant services.Configure(options => { options.ViewLocationExpanders.Insert(0, new TenantViewLocationExpander()); }); // the tenants configuration services.Configure(this.Configuration.GetSection("Tenants")); // configuration specific for tenant abc services.Configure("abc", options => { options.NumberOption = 1; options.StringOption = "abc"; }); // configuration specific for tenant xyz services.Configure("xyz", options => { options.NumberOption = 2; options.StringOption = "xyz"; }); // a multitenant-aware DbContext services.AddDbContext(options => { // the default connection, use whatever you need, if using the different connection per tenant strategy, this will be overridden options.UseSqlServer(this.Configuration.GetConnectionString("DefaultConnection")); }); // the tenant service, required by all the others, entry point for ITenantIdentificationService services.AddScoped<ITenantService, TenantService>(); // the tenant identification/resolution service services.AddScoped<ITenantIdentificationService, HostTenantIdentificationService>(); // the service for applying multitenancy to a multitenant-aware DbContext services.AddSingleton<ITenantDbContext, FilterTenantDbContext>(); // adding tenant-specific configuration classes from a given assembly services.AddTenantConfiguration();
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]