Multi-Tenant Application Architecture
- 7 minutes to read
A Multi-Tenant Application’s Key Components
The diagram below illustrates a multi-tenant application’s key components:
Consider the following specifics that are unique to a multi-tenant application structure:
A multi-tenant application always has a Super Administrator account as well as tenant user accounts.
The Tenant Resolver (an
ITenantResolver
implementation) is used to determine a specific user’s tenant.An XAF application’s code and custom code that implements the application’s business logic can use the Tenant Provider (
ITenantProvider
) service to determine the current tenant. The Object Space Provider (IObjectSpaceProvider) service can be used to access only the current tenant’s data.
Also, see the Glossary to familiarize yourself with terms specific to multi-tenancy in XAF.
Data Access Architecture
A multi-tenant application requires several independent databases:
Host Database
The Host Database stores the following data:
- The list of Host User Interface users (Super Administrators)
- The list of tenants
- The Host Interface’s application model differences
The tenant data stored in the Host Database is defined by the Tenant
type and includes the following data:
- A tenant’s unique ID (
Guid
) - A tenant’s name (
string
) - A connection string for the Tenant Database
A user in Tenant User Interface mode does not have access to this database as well as to databases of other tenants.
One or several Tenant Databases
A tenant database stores the following information:
- The list of the tenant’s users
- The list of roles and access permissions for the tenant
- The tenant’s application model differences
- The data for business objects, on which the tenant’s end-users operate
A Super Administrator user in Host User Interface mode does not have access to data from tenant databases.
User Logon Process
The tenant that a user belongs to is determined during the user logon process. The diagram below illustrates the logon process in a multi-tenant application.
Tenant Resolvers
A Tenant Resolver is used to determine the tenant to which a user belongs. XAF includes the following built-in tenant resolvers:
TenantByEmailResolver
This Tenant Resolver determines the tenant name based on the user login in the following format: "{User}@{Tenant}"
. For example, if a user’s login is “John@company.com”
, the tenant name is determined as "company.com"
and the user name as "John"
. This resolver is used in applications generated by the XAF Solution Wizard.
TenantByUserNameResolver
This Tenant Resolver cannot be used directly but serves as a base class for custom Tenant Resolver implementation. In a descendant, you can define an arbitrary regex pattern to determine the tenant. The pattern string must contain the "{User}"
and "{Tenant}"
parts that stand for the user name and tenant name respectively. For example, consider the following Tennant Resolver implementation:
using DevExpress.ExpressApp.MultiTenancy;
using DevExpress.ExpressApp.Security;
// ...
public class TenantByUserNameResolverEx:TenantByUserNameResolver {
public TenantByUserNameResolverEx(IServiceProvider serviceProvider)
: base(
serviceProvider,
"{User}\\\\{Tenant}",
(IAuthenticationStandardLogonParameters parameters) => parameters.UserName) { }
}
With the specified regex pattern, if a user specifies the "company\John"
login, the user name is determined as "John"
, and the tenant name as "company"
.
Custom Tenant Resolver
If you need to implement a custom algorithm that is not based on templates to determine the tenant, you can implement a custom Tenant Resolver. To do this, create a class that implements the ITenantResolver
interface or extends one of the existing Tenant Resolver classes. For example:
// A resolver that extracts the tenant name from a string of the following format: "TenantName\\UserName".
public class CustomTenantResolver : ITenantResolver {
readonly ITenantNameHelper tenantNameHelper;
public CustomTenantResolver(ITenantNameHelper tenantNameHelper) {
this.tenantNameHelper = tenantNameHelper;
}
#region ITenantResolver implementation
public Guid? GetTenantId(IAuthenticationStandardLogonParameters logonParams) {
string tenantName = GetTenantName(logonParams.UserName);
if(!string.IsNullOrEmpty(tenantName)) {
return tenantNameHelper.GetTenantIdByName(tenantName);
}
return null;
}
public Guid? GetTenantId(string userLogin) {
string tenantName = GetTenantName(userLogin);
if(!string.IsNullOrEmpty(tenantName)) {
return tenantNameHelper.GetTenantIdByName(tenantName);
}
return null;
}
public string FormatUserLogin(string userName, string tenantName) {
return $"{tenantName}\\{userName}";
}
#endregion
string GetTenantName(string userLogin) {
if(userLogin != null) {
int p = userLogin.IndexOf('\\');
if(p > 0) {
return userLogin.Substring(0, p);
}
}
return null;
}
}
A Tenant Resolver’s constructor can access any dependencies (services registered in the application dependency container) through Dependency Injection. In the above code sample, the Tenant Resolver injects the ITenantNameHelper
service. The Tenant Resolver then uses this service to obtain tenants’ unique identifiers.
Specify the custom Tenant Resolver class in code that configures multi-tenancy through the Application Builder as shown below:
File: MySolution.Blazor.Server/Startup.cs (MySolution.Win/Startup.cs)
builder.AddMultiTenancy()
// ...
.WithTenantResolver<CustomTenantResolver>();
Extend the Built-in Tenant Class with Custom Fields
Create and Register a Custom Tenant Class
You can extend the standard Tenant
class to associate additional data with tenants (stored in the application’s Host Database). To do this, follow the steps below.
First, create the standard
Tenant
class descendant. In this new class, implement the required additional fields:File: MySolution.Module\BusinessObjects\CustomTenant.cs
using DevExpress.Persistent.BaseImpl.EF.MultiTenancy; // ... public class CustomTenant : Tenant { public virtual string CustomField { get; set; } }
In the application builder code, use the
WithCustomTenantType
method to register your custom tenant class as shown below:File: MySolution.Blazor.Server/Startup.cs (MySolution.Win/Startup.cs)
// .. builder.AddMultiTenancy() .WithCustomTenantType<CustomTenant>() //...
If the Module Updater code in your application contains tenant creation logic, modify this code to use your custom tenant class instead of
Tenant
. The following code sample demonstrates how to modify Model Updater code generated by the XAF Solution Wizard:File: MySolution.Module/DatabaseUpdate/Updater.cs
public class Updater : ModuleUpdater { public override void UpdateDatabaseAfterUpdateSchema() { base.UpdateDatabaseAfterUpdateSchema(); // ... #if !RELEASE if (TenantName == null) { _ = CreateTenant("company1.com", "MySolution_company1", "customValue1"); _ = CreateTenant("company2.com", "MySolution_company2", "customValue2"); ObjectSpace.CommitChanges(); } #endif } //... private CustomTenant CreateTenant(string tenantName, string databaseName, string customField) { var tenant = ObjectSpace.FirstOrDefault<CustomTenant>(t => t.Name == tenantName); if (tenant == null) { tenant = ObjectSpace.CreateObject<CustomTenant>(); tenant.Name = tenantName; tenant.ConnectionString = $"Integrated Security=SSPI;MultipleActiveResultSets=True;Data Source=(localdb)\\mssqllocaldb;Initial Catalog={databaseName}"; tenant.CustomField = customField; } return tenant; } // ... }
You can now run your application in Host User Interface mode and review the custom tenant records created by the Module Updater:
Access Custom Tenant Fields in Code
Use the ITenantProvider
service’s TenantObject
object property to access the current tenant. You can explicitly cast this property’s value to your custom tenant type and access the tenant’s custom fields directly.
From an XAF View Controller
In a View Controller, you can access the ITenantProvider
service through Dependency Injection:
public class MyViewController : ViewController {
ITenantProvider tenantProvider;
// ...
[ActivatorUtilitiesConstructor]
public MyViewController(IServiceProvider serviceProvider) : this() {
// ...
tenantProvider = serviceProvider.GetService<ITenantProvider>();
}
protected override void OnActivated() {
base.OnActivated();
CustomTenant tenant = (CustomTenant)tenantProvider.TenantObject;
var customField = tenant.CustomField;
// ...
}
}
From Application Builders
You can also access ITenantProvider
in application builder code, and in delegates that receive XafApplication
as a parameter (through the application.ServiceProvider
property). The code example below demonstrates a use case scenario where a custom tenant stores an additional connection string (the tenant object’s SecondConnectionString
property). The application builder code uses this connection string to additionally configure Object Space Providers.
File: MySolution.Blazor.Server/Startup.cs (MySolution.Win/Startup.cs)
builder.ObjectSpaceProviders
// ...
.AddSecuredEFCore(options => options.PreFetchReferenceProperties())
.WithDbContext<SecondDbContext>((application, options) => {
// Retrieve the current tenant object
var tenant = (CustomTenant)application.ServiceProvider.GetRequiredService<ITenantProvider>().TenantObject;
string secondConnectionString;
if(tenant == null) {
// If running in Host User Interface mode, do not use an additional connection string
secondConnectionString = connectionString;
}
else {
// If running in Tenant User Interface mode, use the additional connection string from the Tenant's custom property
secondConnectionString = tenant.SecondConnectionString;
}
options.UseSqlServer(secondConnectionString);
options.UseChangeTrackingProxies();
options.UseObjectSpaceLinkProxies();
}, ServiceLifetime.Transient)
// ...
See the following topic for more information: Get the Current Tenant in Code.