Convert an Existing Application into a Multi-Tenant Application
- 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.v24.1
XPO: DevExpress.ExpressApp.MultiTenancy.XPO.v24.1 - SolutionName.Blazor.Server (the Blazor application project)
- EF Core: DevExpress.ExpressApp.MultiTenancy.Blazor.EFCore.v24.1
XPO: DevExpress.ExpressApp.MultiTenancy.Blazor.XPO.v24.1 - SolutionName.Win (the WinForms application project)
- EF Core: DevExpress.ExpressApp.MultiTenancy.Win.EFCore.v24.1
XPO: DevExpress.ExpressApp.MultiTenancy.Win.XPO.v24.1 - SolutionName.WebApi (the Web API Service project)
- EF Core: DevExpress.ExpressApp.MultiTenancy.WebApi.EFCore.v24.1
XPO: DevExpress.ExpressApp.MultiTenancy.WebApi.Xpo.v24.1 - SolutionName.MiddleTier (the Middle Tier Security project)
- EF Core: DevExpress.ExpressApp.MultiTenancy.AspNetCore.EFCore.v24.1
XPO: DevExpress.ExpressApp.MultiTenancy.AspNetCore.Xpo.v24.1
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).
{
"ConnectionStrings": {
"ConnectionString": "Integrated Security=SSPI;Pooling=true;MultipleActiveResultSets=true;Data Source=(localdb)\\mssqllocaldb;Initial Catalog=MySolution_Service",
},
Enable Multi-Tenancy Mode
In the application’s Startup.cs file, add code that enables and configures multi-tenancy mode:
EF Core
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
- C# (Blazor)
- C# (WinForms)
- C# (Web API Service)
- C# (Middle Tier Security)
- C# (Windows Forms applications with Integrated Mode)
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddXaf(Configuration, builder => {
// ...
builder.AddMultiTenancy()
.WithHostDbContext((sp, opt) => {
opt.UseSqlServer(Configuration.GetConnectionString("ConnectionString"));
opt.UseLazyLoadingProxies();
opt.UseChangeTrackingProxies();
})
.WithTenantResolver<TenantByEmailResolver>();
// ...
});
}
}
XPO
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
- C# (Blazor)
- C# (WinForms)
- C# (Web API Service)
- C# (Middle Tier Security)
- C# (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>();
// ...
});
}
}
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)
.AddSecuredEFCore(options => options.PreFetchReferenceProperties())
.WithDbContext<SolutionEFCoreDbContext>((application, options) => {
string connectionString = application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString();
options.UseSqlServer(connectionString);
options.UseChangeTrackingProxies();
options.UseObjectSpaceLinkProxies();
}, ServiceLifetime.Transient)
.AddNonPersistent();
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
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
// ...
}
}
// ...
}