Back to Devexpress

Convert an Existing Application into a Multi-Tenant Application

expressappframework-404670-multitenancy-convert-existing-application-into-multi-tenant-application.md

latest14.2 KB
Original Source

Convert an Existing Application into a Multi-Tenant Application

  • Jul 09, 2025
  • 6 minutes to read

Follow the steps below to convert an existing XAF application to a multi-tenant application.

Install Required Dependencies

Install the following NuGet packages:

SolutionName.Module (the shared module project) EF Core : DevExpress.ExpressApp.MultiTenancy.EFCore.v25.2
XPO : DevExpress.ExpressApp.MultiTenancy.XPO.v25.2SolutionName.Blazor.Server (the Blazor application project) EF Core : DevExpress.ExpressApp.MultiTenancy.Blazor.EFCore.v25.2
XPO : DevExpress.ExpressApp.MultiTenancy.Blazor.XPO.v25.2SolutionName.Win (the WinForms application project) EF Core : DevExpress.ExpressApp.MultiTenancy.Win.EFCore.v25.2
XPO : DevExpress.ExpressApp.MultiTenancy.Win.XPO.v25.2SolutionName.WebApi (the Web API Service project) EF Core : DevExpress.ExpressApp.MultiTenancy.WebApi.EFCore.v25.2
XPO : DevExpress.ExpressApp.MultiTenancy.WebApi.Xpo.v25.2SolutionName.MiddleTier (the Middle Tier Security project) EF Core : DevExpress.ExpressApp.MultiTenancy.AspNetCore.EFCore.v25.2
XPO : DevExpress.ExpressApp.MultiTenancy.AspNetCore.Xpo.v25.2

Add a Connection String for the Host Database

Edit the application’s configuration file:

  • appsettings.json in Blazor, Web API Service, Middle Tier Security
  • App.config in WinForms

Add a connection string for the Host Database (named "ConnectionString" throughout this article).

json
{
  "ConnectionStrings": {
    "ConnectionString": "Integrated Security=SSPI;Pooling=true;MultipleActiveResultSets=true;Data Source=(localdb)\\mssqllocaldb;Initial Catalog=MySolution_Service",
  },
xml
<configuration>
  <connectionStrings>
    <add name="ConnectionString" connectionString="Integrated Security=SSPI;MultipleActiveResultSets=True;Data Source=(localdb)\mssqllocaldb;Initial Catalog=MySolution_Service;Application Name=MySolution" providerName="System.Data.SqlClient" />
  </connectionStrings>
</configuration>

Enable Multi-Tenancy Mode

In the application’s Startup.cs file, add code that enables and configures multi-tenancy mode:

EF Core

File:

csharp
public class Startup { 
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy() 
                .WithHostDbContext((sp, opt) => {
                    opt.UseConnectionString(Configuration.GetConnectionString("ConnectionString")); 
                })
                .WithTenantResolver<TenantByEmailResolver>(); 
             // ...
        });
    }
}
csharp
public class ApplicationBuilder : IDesignTimeApplicationFactory {
    public static WinApplication BuildApplication(string connectionString) {
        var builder = WinApplication.CreateBuilder();
        // ...
        builder.AddMultiTenancy()
            .WithHostDbContext((serviceProvider, options) => { 
                options.UseConnectionString(connectionString);
            }) 
            .WithTenantResolver<TenantByEmailResolver>();
        // ...
    }
}
csharp
public class Startup { 
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy() 
                .WithHostDbContext((serviceProvider, options) => {
                    string connectionString = Configuration.GetConnectionString("ConnectionString");
                    options.UseConnectionString(connectionString);
                })
                .WithMultiTenancyModelDifferenceStore()
                .WithTenantResolver<TenantByEmailResolver>();
             // ...
        });
    }
}
csharp
public class Startup { 
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy() 
                .WithHostDbContext((serviceProvider, options) => {
                    string connectionString = Configuration.GetConnectionString("ConnectionString");
                    options.UseConnectionString(connectionString);
                })
                .WithMultiTenancyModelDifferenceStore()
                .WithTenantResolver<TenantByEmailResolver>();
             // ...
        });
    }
}
csharp
public class ApplicationBuilder : IDesignTimeApplicationFactory {
    public static WinApplication BuildApplication(string connectionString) {
        var builder = WinApplication.CreateBuilder();
        // ...
        builder.AddMultiTenancy()
            .WithHostDbContext((serviceProvider, options) => { 
                options.UseMiddleTier(serviceProvider.GetRequiredService<ISecurityStrategyBase>());
                options.UseChangeTrackingProxies();
            }, true)
            .WithMultiTenancyModelDifferenceStore()
            .WithTenantResolver<TenantByEmailResolver>();
        // If you register UserType and RoleType on server side only, add them to the options on client side too:
        // builder.Get().PostConfigure<SecurityOptions>(options => {
        // options.RoleType = typeof(PermissionPolicyRole);
        // options.UserType = typeof(Module.BusinessObjects.ApplicationUser);
        // });
    }
}

XPO

File:

csharp
public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy()
                .WithHostDatabaseConnectionString(Configuration.GetConnectionString("ConnectionString"))
                .WithTenantResolver<TenantByEmailResolver>();
            // ...
        });
    }
}
csharp
public class ApplicationBuilder : IDesignTimeApplicationFactory {
    public static WinApplication BuildApplication(string connectionString) {
        var builder = WinApplication.CreateBuilder();
        // ...
        builder.AddMultiTenancy()
            .WithHostDatabaseConnectionString(connectionString)
            .WithTenantResolver<TenantByEmailResolver>();
        // ...
    }
}
csharp
public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy()
                // In Web API Service and Windows Forms with Middle Tier Security projects, XAF creates and checks the database only once at setup.
                // If you need to check compatibility for each tenant's database, add this method:
                //.WithTenantDatabaseUpdater()
                .WithHostDatabaseConnectionString(Configuration.GetConnectionString("ConnectionString"))
                .WithMultiTenancyModelDifferenceStore()
                .WithTenantResolver<TenantByEmailResolver>();
            // ...
        });
    }
}
csharp
public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        services.AddXaf(Configuration, builder => {
            // ...
            builder.AddMultiTenancy()
                // In Web API Service and Windows Forms with Middle Tier Security projects, XAF creates and checks the database only once at setup.
                // If you need to check compatibility for each tenant's database, add this method:
                //.WithTenantDatabaseUpdater()
                .WithHostDatabaseConnectionString(Configuration.GetConnectionString("ConnectionString"))
                .WithMultiTenancyModelDifferenceStore()
                .WithTenantResolver<TenantByEmailResolver>();
            // ...
        });
    }
}
csharp
public class ApplicationBuilder : IDesignTimeApplicationFactory {
    public static WinApplication BuildApplication(string connectionString) {
        var builder = WinApplication.CreateBuilder();
        // ...
        builder.AddMultiTenancy(true)
            .WithMultiTenancyModelDifferenceStore()
            .WithTenantResolver<TenantByEmailResolver>();
        // If you register UserType and RoleType on server side only, add them to the options on client side too:
        // builder.Get().PostConfigure<SecurityOptions>(options => {
        // options.RoleType = typeof(PermissionPolicyRole);
        // options.UserType = typeof(Module.BusinessObjects.ApplicationUser);
        // });
        // ...
    }
}

Modify Code that Configures ObjectSpaceProviders

In a multi-tenant application, the connection string for a database that stores tenant data is not static and changes based on the current tenant. To accommodate for this, make the following changes to methods that configure the application’s ObjectSpaceProviders:

  • Use the IConnectionStringProvider service to obtain the connection string instead of assigning the connection string as a static value.
  • In a WinForms application that uses EF Core, specify an additional parameter for created ObjectSpaceProviders: serviceLifeTime = ServiceLifeTime.Transient.

Below is an example of modified code used to configure ObjectSpaceProviders in a multi-tenant application.

File: MySolution.Blazor.Server/Startup.cs (MySolution.Win/Startup.cs)

csharp
.AddSecuredEFCore(options => options.PreFetchReferenceProperties())
    .WithDbContext<SolutionEFCoreDbContext>((application, options) => {
        string connectionString = application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString();
        options.UseConnectionString(connectionString);
        options.UseObjectSpaceLinkProxies();
    }, ServiceLifetime.Transient)
.AddNonPersistent();
csharp
builder.ObjectSpaceProviders
    .AddSecuredXpo((application, options) => {
        string connectionString = application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString();
        options.ConnectionString = connectionString;
        // ...
    })

Modify the Module Updater Code

A multi-tenant application works with multiple databases that have separate purposes and store separate data sets. Consequently, code that updates a database must take into account what tenant is currently active and whether or not the application runs in Host User Interface mode. In most cases, you need to follow the rules below when you modify the Module Updater Code to use it in a multi-tenant application.

  • When you update the Host Database, do not create any business objects used in Tenant Databases.
  • When you update the Host Database, do not create user accounts with roles other than the Administrators role.
  • When you update a Tenant Database, specify user logins in the format from which the format can be determined. For more information, see: Tenant Resolvers.

To determine the configuration in which the application currently runs, you can use the following custom properties in your ModuleUpdater descendant:

File: MySolution.Module/DatabaseUpdate/Updater.cs

csharp
public class Updater : ModuleUpdater {
    // Returns the current tenant's unique identifier.
    // If `null`, the application runs in Host User Interface mode.
    Guid? TenantId {
        get {
            return ObjectSpace.ServiceProvider?.GetService<ITenantProvider>()?.TenantId;
        }
    }

    // Returns the current tenant's name.
    // If `null`, the application runs in Host User Interface mode.
    string TenantName {
        get {
            return ObjectSpace.ServiceProvider?.GetService<ITenantProvider>()?.TenantName;
        }
    }
}

The code sample below demonstrates how you can modify the UpdateDatabaseAfterUpdateSchema method implementation to use it in a multi-tenant application:

File: MySolution.Module/DatabaseUpdate/Updater.cs

csharp
public class Updater : ModuleUpdater {
    public override void UpdateDatabaseAfterUpdateSchema() {
        base.UpdateDatabaseAfterUpdateSchema();
        if (TenantId != null) {
            // Code that updates Tenant Databases
            if(TenantName == "company1.com") {
                // Code that updates the database for the tenant `company1.com`
            }
            // ...
        } else {
            // Code that updates the Host Database
            // ...
        }
    }
    // ...
}

See Also

Create a new Multi-Tenant Application