expressappframework-404670-multitenancy-convert-existing-application-into-multi-tenant-application.md
Follow the steps below to convert an existing XAF application to a multi-tenant application.
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
Edit the application’s configuration file:
Add a connection string for the Host Database (named "ConnectionString" throughout this article).
{
"ConnectionStrings": {
"ConnectionString": "Integrated Security=SSPI;Pooling=true;MultipleActiveResultSets=true;Data Source=(localdb)\\mssqllocaldb;Initial Catalog=MySolution_Service",
},
<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>
In the application’s Startup.cs file, add code that enables and configures multi-tenancy mode:
File:
MySolution.Blazor.Server/Startup.cs
MySolution.Win/Startup.cs
MySolution.WebApi/Startup.cs
MySolution.MiddleTier/Startup.cs
MySolution.Win/Startup.cs in Windows Forms applications with Integrated Mode
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddXaf(Configuration, builder => {
// ...
builder.AddMultiTenancy()
.WithHostDbContext((sp, opt) => {
opt.UseConnectionString(Configuration.GetConnectionString("ConnectionString"));
})
.WithTenantResolver<TenantByEmailResolver>();
// ...
});
}
}
public class ApplicationBuilder : IDesignTimeApplicationFactory {
public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
// ...
builder.AddMultiTenancy()
.WithHostDbContext((serviceProvider, options) => {
options.UseConnectionString(connectionString);
})
.WithTenantResolver<TenantByEmailResolver>();
// ...
}
}
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>();
// ...
});
}
}
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>();
// ...
});
}
}
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);
// });
}
}
File:
MySolution.Blazor.Server/Startup.cs
MySolution.Win/Startup.cs
MySolution.WebApi/Startup.cs
MySolution.MiddleTier/Startup.cs
MySolution.Win/Startup.cs in Windows Forms applications with Integrated Mode
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddXaf(Configuration, builder => {
// ...
builder.AddMultiTenancy()
.WithHostDatabaseConnectionString(Configuration.GetConnectionString("ConnectionString"))
.WithTenantResolver<TenantByEmailResolver>();
// ...
});
}
}
public class ApplicationBuilder : IDesignTimeApplicationFactory {
public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
// ...
builder.AddMultiTenancy()
.WithHostDatabaseConnectionString(connectionString)
.WithTenantResolver<TenantByEmailResolver>();
// ...
}
}
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>();
// ...
});
}
}
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>();
// ...
});
}
}
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);
// });
// ...
}
}
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:
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)
.AddSecuredEFCore(options => options.PreFetchReferenceProperties())
.WithDbContext<SolutionEFCoreDbContext>((application, options) => {
string connectionString = application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString();
options.UseConnectionString(connectionString);
options.UseObjectSpaceLinkProxies();
}, ServiceLifetime.Transient)
.AddNonPersistent();
builder.ObjectSpaceProviders
.AddSecuredXpo((application, options) => {
string connectionString = application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString();
options.ConnectionString = connectionString;
// ...
})
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.
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
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
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