Multi-Tenancy

ABP Multi-tenancy module provides base functionality to create multi tenant applications.

Wikipedia defines multi-tenancy as like that:

Software Multi-tenancy refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a dedicated share of the instance including its data, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.

Volo.Abp.MultiTenancy Package

Volo.Abp.MultiTenancy package defines fundamental interfaces to make your code “multi-tenancy ready”. So, install it to your project using the package manager console (PMC):

  1. Install-Package Volo.Abp.MultiTenancy

This package is already installed by default with the startup template. So, most of the time, you don’t need to install it manually.

Then you can add AbpMultiTenancyModule dependency to your module:

  1. using Volo.Abp.Modularity;
  2. using Volo.Abp.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. [DependsOn(typeof(AbpMultiTenancyModule))]
  6. public class MyModule : AbpModule
  7. {
  8. //...
  9. }
  10. }

With the “Multi-tenancy ready” concept, we intent to develop our code to be compatible with multi-tenancy approach. Then it can be used in a multi-tenant application or not, depending on the requirements of the final application.

Define Entities

You can implement IMultiTenant interface for your entities to make them multi-tenancy ready. Example:

  1. using System;
  2. using Volo.Abp.Domain.Entities;
  3. using Volo.Abp.MultiTenancy;
  4. namespace MyCompany.MyProject
  5. {
  6. public class Product : AggregateRoot, IMultiTenant
  7. {
  8. public Guid? TenantId { get; set; } //IMultiTenant defines TenantId property
  9. public string Name { get; set; }
  10. public float Price { get; set; }
  11. }
  12. }

IMultiTenant requires to define a TenantId property in the implementing entity (See entity documentation for more about entities).

Obtain Current Tenant’s Id

Your code may require to get current tenant’s id (regardless of how it’s retrieved actually). You can inject and use ICurrentTenant interface for such cases. Example:

  1. using Volo.Abp.DependencyInjection;
  2. using Volo.Abp.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. public class MyService : ITransientDependency
  6. {
  7. private readonly ICurrentTenant _currentTenant;
  8. public MyService(ICurrentTenant currentTenant)
  9. {
  10. _currentTenant = currentTenant;
  11. }
  12. public void DoIt()
  13. {
  14. var tenantId = _currentTenant.Id;
  15. //use tenantId in your code...
  16. }
  17. }
  18. }

Change Current Tenant

TODO: …

Determining Current Tenant

The first thing for a multi-tenant application is to determine the current tenant on the runtime. Volo.Abp.MultiTenancy package only provides abstractions (named as tenant resolver) for determining the current tenant, however it does not have any implementation out of the box.

Volo.Abp.AspNetCore.MultiTenancy package has implementation to determine the current tenant from current web request (from subdomain, header, cookie, route… etc.). See Volo.Abp.AspNetCore.MultiTenancy Package section later in this document.

Custom Tenant Resolvers

You can add your custom tenant resolver to AbpTenantResolveOptions in your module’s ConfigureServices method as like below:

  1. using Microsoft.Extensions.DependencyInjection;
  2. using Volo.Abp.Modularity;
  3. using Volo.Abp.MultiTenancy;
  4. namespace MyCompany.MyProject
  5. {
  6. [DependsOn(typeof(AbpMultiTenancyModule))]
  7. public class MyModule : AbpModule
  8. {
  9. public override void ConfigureServices(ServiceConfigurationContext context)
  10. {
  11. Configure<AbpTenantResolveOptions>(options =>
  12. {
  13. options.TenantResolvers.Add(new MyCustomTenantResolveContributor());
  14. });
  15. //...
  16. }
  17. }
  18. }

MyCustomTenantResolveContributor must implement ITenantResolveContributor as shown below:

  1. using Volo.Abp.MultiTenancy;
  2. namespace MyCompany.MyProject
  3. {
  4. public class MyCustomTenantResolveContributor : ITenantResolveContributor
  5. {
  6. public void Resolve(ITenantResolveContext context)
  7. {
  8. context.TenantIdOrName = ... //find tenant id or tenant name from somewhere...
  9. }
  10. }
  11. }

A tenant resolver can set TenantIdOrName if it can determine it. If not, just leave it as is to allow next resolver to determine it.

Tenant Store

Volo.Abp.MultiTenancy package defines ITenantStore to abstract data source from the framework. You can implement ITenantStore to work with any data source (like a relational database) that stores information of your tenants.

Configuration Data Store

There is a built in (and default) tenant store, named ConfigurationTenantStore, that can be used to store tenants using standard configuration system (with Microsoft.Extensions.Configuration package). Thus, you can define tenants as hard coded or get from your appsettings.json file.

Example: Define tenants as hard-coded
  1. using System;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Volo.Abp.Data;
  4. using Volo.Abp.Modularity;
  5. using Volo.Abp.MultiTenancy;
  6. namespace MyCompany.MyProject
  7. {
  8. [DependsOn(typeof(AbpMultiTenancyModule))]
  9. public class MyModule : AbpModule
  10. {
  11. public override void ConfigureServices(ServiceConfigurationContext context)
  12. {
  13. Configure<AbpDefaultTenantStoreOptions>(options =>
  14. {
  15. options.Tenants = new[]
  16. {
  17. new TenantConfiguration(
  18. Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id
  19. "tenant1" //Name
  20. ),
  21. new TenantConfiguration(
  22. Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id
  23. "tenant2" //Name
  24. )
  25. {
  26. //tenant2 has a seperated database
  27. ConnectionStrings =
  28. {
  29. {ConnectionStrings.DefaultConnectionStringName, "..."}
  30. }
  31. }
  32. };
  33. });
  34. }
  35. }
  36. }
Example: Define tenants in appsettings.json

First create your configuration from your appsettings.json file as you always do.

  1. using System.IO;
  2. using Microsoft.Extensions.Configuration;
  3. using Microsoft.Extensions.DependencyInjection;
  4. using Volo.Abp.Modularity;
  5. using Volo.Abp.MultiTenancy;
  6. namespace MyCompany.MyProject
  7. {
  8. [DependsOn(typeof(AbpMultiTenancyModule))]
  9. public class MyModule : AbpModule
  10. {
  11. public override void ConfigureServices(ServiceConfigurationContext context)
  12. {
  13. var configuration = BuildConfiguration();
  14. Configure<AbpDefaultTenantStoreOptions>(configuration);
  15. }
  16. private static IConfigurationRoot BuildConfiguration()
  17. {
  18. return new ConfigurationBuilder()
  19. .SetBasePath(Directory.GetCurrentDirectory())
  20. .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
  21. .Build();
  22. }
  23. }
  24. }

Then add a “Tenants“ section to your appsettings.json:

  1. "Tenants": [
  2. {
  3. "Id": "446a5211-3d72-4339-9adc-845151f8ada0",
  4. "Name": "tenant1"
  5. },
  6. {
  7. "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
  8. "Name": "tenant2",
  9. "ConnectionStrings": {
  10. "Default": "...write tenant2's db connection string here..."
  11. }
  12. }
  13. ]
Volo.Abp… Package (TODO)

TODO: This package implements ITenantStore using a real database…

Tenant Information

ITenantStore works with TenantConfiguration class that has several properties for a tenant:

  • Id: Unique Id of the tenant.
  • Name: Unique name of the tenant.
  • ConnectionStrings: If this tenant has dedicated database(s) to store it’s data, then connection strings can provide database connection strings (it may have a default connection string and connection strings per modules - TODO: Add link to Abp.Data package document).

A multi-tenant application may require additional tenant properties, but these are the minimal requirements for the framework to work with multiple tenants.

Change Tenant By Code

TODO…

Volo.Abp.AspNetCore.MultiTenancy Package

Volo.Abp.AspNetCore.MultiTenancy package integrate multi-tenancy to ASP.NET Core applications. To install it to your project, run the following command on PMC:

  1. Install-Package Volo.Abp.AspNetCore.MultiTenancy

Then you can add AbpAspNetCoreMultiTenancyModule dependency to your module:

  1. using Volo.Abp.Modularity;
  2. using Volo.Abp.AspNetCore.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
  6. public class MyModule : AbpModule
  7. {
  8. //...
  9. }
  10. }

Multi-Tenancy Middleware

Volo.Abp.AspNetCore.MultiTenancy package includes the multi-tenancy middleware…

  1. app.UseMultiTenancy();

TODO:…

Determining Current Tenant From Web Request

Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box:

  • CurrentUserTenantResolveContributor: Gets the tenant id from claims of the current user, if the current user has logged in. This should always be the first contributor for security.
  • QueryStringTenantResolveContributor: Tries to find current tenant id from query string parameter. Parameter name is “__tenant” by default.
  • RouteTenantResolveContributor: Tries to find current tenant id from route (URL path). Variable name is “__tenant” by default. So, if you defined a route with this variable, then it can determine the current tenant from the route.
  • HeaderTenantResolveContributor: Tries to find current tenant id from HTTP header. Header name is “__tenant” by default.
  • CookieTenantResolveContributor: Tries to find current tenant id from cookie values. Cookie name is “__tenant” by default.

If you use nginx as a reverse proxy server, please note that if TenantKey contains an underscore or other special characters, there may be a problem, please refer to: http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers

“__tenant” parameter name can be changed using AbpAspNetCoreMultiTenancyOptions. Example:

  1. services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
  2. {
  3. options.TenantKey = "MyTenantKey";
  4. });
Domain Tenant Resolver

In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure AbpTenantResolveOptions to add a domain tenant resolver.

Example: Add a subdomain resolver
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Volo.Abp.AspNetCore.MultiTenancy;
  3. using Volo.Abp.Modularity;
  4. using Volo.Abp.MultiTenancy;
  5. namespace MyCompany.MyProject
  6. {
  7. [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
  8. public class MyModule : AbpModule
  9. {
  10. public override void ConfigureServices(ServiceConfigurationContext context)
  11. {
  12. Configure<AbpTenantResolveOptions>(options =>
  13. {
  14. //Subdomain format: {0}.mydomain.com
  15. //Adding as the second highest priority resolver after 'CurrentUserTenantResolveContributor' to
  16. //ensure the user cannot impersonate a different tenant.
  17. options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));
  18. });
  19. //...
  20. }
  21. }
  22. }

{0} is the placeholder to determine current tenant’s unique name.

Instead of options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com")); you can use this shortcut:

  1. options.AddDomainTenantResolver("{0}.mydomain.com");
Example: Add a domain resolver
  1. options.AddDomainTenantResolver("{0}.com");

See Also