Skip to main content
All docs
V23.2

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:

Databases in a Multi-Tenant Application

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.

Multi-Tenant Application Logon

Tenant Resolvers

A Tenant Resolver is used to determine the tenant to which a user belongs. XAF includes the following built-in tenant resolvers:

TenantByUserNameResolver

This Tenant Resolver determines the tenant name based on the user login. The resolver’s logic uses an arbitrary specified text template to determine the tenant. The template string must contain the "{User}" and "{Tenant}" parts that stand for the user name and tenant name respectively. For example, consider the following template string:

{Tenant}\\{User}

With this template, if a user specifies the "company\John" login, the user name is determined as "John", and the tenant name as "company".

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.

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.

  1. 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; }
    }
    
  2. 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>()
        //...
    
  3. 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:

Tenants List View

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.

See Also