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.
Properties | Description |
---|---|
Id | Id of tenant. |
TenancyName | Tenancy name of tenant. |
BranchNo | Display name of the Tenant. |
SubscriptionExpireDate | Represents 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;
Properties | Description | Type |
---|---|---|
Id | Id of tenant. | TenantId |
TenancyName | Tenancy name of tenant. | string |
BranchNo | Branch number of tenant. | int |
Name | Name of tenant. | string |
TenancyName | Display name of tenant. | string |
ConnectionString | Database connection string of tenant. | string |
SubscriptionExpireDate | Subscription expiration date of tenant. | DateTime |
IsActive | Determines tenant is active or not. | bool |
Modules | Purchased modules of tenant. | List < string > |
Settings | Application 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();