Skip to main content
Version: 1.0.1

Multi Tenancy

What is Multi Tenancy?

It’s a single codebase that responds differently depending on which “tenant” is accessing it, there’s a few different patterns you can use like

Application level isolation: Spin up a new website and associated dependencies for each tenant.
Multi-tenant app each with their own database: Tenants use the same website, but have their own database.
Multi-tenant app with multi-tenant database: Tenants use the same website and the same database.

What’s required in a multi-tenat app?

There’s a few core requirements a multi-teant app will need to meet.

Tenant resolution

From the HTTP Request we will need to be able to decide which tenant context to run the request under. This impacts things like which database to access, or what configuration to use.

Per-tenant app configuration

The application might be configured differently depending on which tenant context is loaded, e.g. Authentication keys for OAuth providers, connection strings etc.

Per-tenant data isolation

A tenant will need to be able to access their data, and their data alone. This could be achieved by partitioning data within a single datastore or by using a datastore per-tenant. Whatever pattern we use we should make it difficult for a developer to expose data in cross tenant scenarios to avoid coding errors.

Tenant Resolution

With any multi-tenant application we need to be able to identify which tenant a request is running under, but before we get too excited we need to decide what data we require to be able to look up a tenant. We really just need one piece of information at this stage, the tenant identifier.

Milva Tenant

Represents a Tenant of the application.

PropertiesDescription
IdId of tenant.
TenancyNameTenancy name of tenant.
BranchNoDisplay name of the Tenant.
SubscriptionExpireDateRepresents Tenant's subscription expire date.

Common tenant resolution strategies

We will use a resolution strategy to match a request to a tenant, the strategy should not rely on any external data to make it nice and fast.

Host header

The tenant will be inferred based on the host header sent by the browser, this is perfect if all your tenants have different domains e.g. https://host1.example.com,https://host2.example.com or https://host3.com if you are supporting custom domains.

E.g. if the host header was https://host1.example.com we would load the Tenant with the Identifier holding the value host1.example.com.

Request Path

The tenant could be inferred based on the route, e.g. https://example.com/host1/...

Header value

The tenant could be inferred based on a header value e.g. x-tenant: host1, this might be useful if all the tenants are accessable on a core api like https://api.example.com and the client can specify the tenant to use with a specific header.

Tenant Accessor

Tenant accessor for easy access.

You can use it in your services by defining it as follows;

private readonly ITenantAccessor<CachedTenant, TenantId> _tenantAccessor;

For example;

_tenantAccessor.GetTenantId();

Cached Tenant

Cached tenant model for distributed redis server.

Example cached tenant;

PropertiesDescriptionType
IdId of tenant.TenantId
TenancyNameTenancy name of tenant.string
BranchNoBranch number of tenant.int
NameName of tenant.string
TenancyNameDisplay name of tenant.string
ConnectionStringDatabase connection string of tenant.string
SubscriptionExpireDateSubscription expiration date of tenant.DateTime
IsActiveDetermines tenant is active or not.bool
ModulesPurchased modules of tenant.List < string >
SettingsApplication general settings for cached tenant.SettingsDTO

This class inherits from IMilvaTenantBase.

Tenant Id

TenantId consists of TenancyName and BranchNo.
Remarks:

 milvasoft    _    1
_________ _
| |
TenancyName BranchNo

This class has different operators.

==

Indicates whether the values of two specified TenantId objects are equal.

!=

Indicates whether the values of two specified TenantId objects are not equal.

<

Indicates whether the values of a.BranchNo are smaller than b.

>

Indicates whether the values of a.BranchNo are bigger than b.

<=

Indicates whether the values of a.BranchNo are smaller or equal than b.

>=

Indicates whether the values of a.BranchNo are bigger or equal than b.

++

Increases tenantId branchNo.

--

Descreases tenantId branchNo.

Cached Tenant Store

Cached tenant store. Sample store for ITenantStore{TTenant, TKey}.

Get Tenant

Returns a tenant according to identifier.

public async Task<TTenant> GetTenantAsync(TKey identifier)

Set Tenant

public async Task<bool> SetTenantAsync(TKey identifier, TTenant tenant)

Here are the steps you need to do to add multi tenancy to your project;

1. You must add the necessary adjustments to Program.cs

public static IHostBuilder CreateWebHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls("https://*:5001","http://*:5000")
.UseWebRoot("wwwroot")
.UseStartup<Startup>();

}).UseServiceProviderFactory(new MultiTenantServiceProviderFactory<CachedTenant, TenantId>(Startup.ConfigureMultitenantContainer));

2. You should inject the necessary services into ConfigureService in Startup.

services.AddMultiTenancy();

3. You should add ConfigureMultitenantContainer to Startup.

public static void ConfigureMultitenantContainer(CachedTenant tenant, ContainerBuilder containerBuilder)
{
containerBuilder.Register(container =>
{
var optionsBuilder = new DbContextOptionsBuilder<YourDbContext>();

optionsBuilder.UseNpgsql(tenant.ConnectionString, b => b.MigrationsAssembly("Your.API").EnableRetryOnFailure()).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

return new YourDbContext(optionsBuilder.Options, container.Resolve<IHttpContextAccessor>(), container.Resolve<IAuditConfiguration>());
}).InstancePerLifetimeScope();
}

You should use the necessary UseMultiTenancy into Configure in Startup.

app.UseMultiTenancy();