xaf-multi-tenant
XAF: Multi-Tenancy
Overview
XAF (v24.2+) built-in multi-tenancy for SaaS apps. One deployed instance serves multiple independent organizations (tenants) with strict database isolation (separate DB per tenant).
Key characteristics:
- Blazor Server (ASP.NET Core) — primary supported platform
- Two operating modes: Host UI (tenant management) and Tenant UI (tenant data)
- Tenant connection strings stored in the host database
- Built-in tenant resolver infrastructure
Approaches
| Approach | Isolation | Notes |
|---|---|---|
| Separate DB per tenant | Full | Default XAF approach; maximum isolation |
| Shared DB with discriminator | Partial | Not natively supported — must implement manually |
| Hybrid | Mixed | Shared config in host DB; operational data in tenant DBs |
Setup (Blazor / v24.2+)
dotnet add package DevExpress.ExpressApp.MultiTenancy
// Program.cs
builder.Services.AddXaf(builder.Configuration, b => {
b.UseApplication<MyBlazorApplication>();
b.AddObjectSpaceProviders(providers => {
providers.UseEntityFramework(ef => {
ef.DefaultDatabaseConnection("HostConnection", p => p
.UseDbContext<HostDbContext>());
});
});
b.Security
.UseIntegratedMode(options => {
options.RoleType = typeof(PermissionPolicyRole);
options.UserType = typeof(ApplicationUser);
})
.AddPasswordAuthentication();
b.AddMultiTenancy()
.WithTenantResolver<TenantByEmailResolver>(); // {User}@{Tenant}
});
Built-in Tenant Resolvers
| Resolver | Login Pattern | Example |
|---|---|---|
TenantByEmailResolver |
{User}@{Tenant} |
john@acme → tenant = "acme" |
TenantByUserNameResolver |
Custom regex | acme//john → tenant = "acme" |
Custom Tenant Resolver
using DevExpress.ExpressApp.MultiTenancy;
public class TenantBySubdomainResolver : ITenantResolver {
readonly IHttpContextAccessor _httpContext;
public TenantBySubdomainResolver(IHttpContextAccessor httpContext) {
_httpContext = httpContext;
}
public Task<string?> GetTenantNameAsync(string? loginName) {
var host = _httpContext.HttpContext?.Request.Host.Host ?? "";
var subdomain = host.Split('.').FirstOrDefault();
return Task.FromResult<string?>(subdomain);
}
}
// Register:
b.AddMultiTenancy().WithTenantResolver<TenantBySubdomainResolver>();
ITenantProvider Interface
public interface ITenantProvider {
Guid? TenantId { get; } // null in Host UI or not logged in
string? TenantName { get; } // null in Host UI or not logged in
object? TenantObject { get; } // the Tenant persistent object
}
Getting the Current Tenant
From ObjectSpace:
var tenantProvider = objectSpace.ServiceProvider.GetService<ITenantProvider>();
Guid? tenantId = tenantProvider?.TenantId;
string? tenantName = tenantProvider?.TenantName;
In Controllers via DI:
public class TenantAwareController : ViewController {
ITenantProvider _tenantProvider;
[ActivatorUtilitiesConstructor]
public TenantAwareController(IServiceProvider serviceProvider) : base() {
_tenantProvider = serviceProvider.GetRequiredService<ITenantProvider>();
}
protected override void OnActivated() {
base.OnActivated();
var tenantName = _tenantProvider.TenantName;
// null → Host UI context
}
}
Tenant-Aware Connection Strings
Each tenant's connection string is stored as a property of the Tenant object in the host DB. XAF uses IConnectionStringProvider to route requests to the correct tenant DB automatically.
// XAF handles routing automatically. Programmatic access if needed:
var connectionStringProvider = serviceProvider.GetService<IConnectionStringProvider>();
string? connStr = connectionStringProvider?.GetConnectionString();
Custom Tenant Class
public class CustomTenant : Tenant {
public virtual string? LogoUrl { get; set; }
public virtual string? PrimaryColor { get; set; }
public virtual int MaxUsers { get; set; }
}
// Register:
b.AddMultiTenancy()
.WithCustomTenantType<CustomTenant>()
.WithTenantResolver<TenantByEmailResolver>();
Access:
var tenant = tenantProvider.TenantObject as CustomTenant;
string? logoUrl = tenant?.LogoUrl;
Data Isolation
// In Tenant UI: queries ONLY the current tenant's database — automatic
var orders = objectSpace.GetObjects<Order>();
// Guard for type availability in current context
if (objectSpace.CanInstantiate(typeof(Order))) {
// safe in this context
}
No built-in cross-tenant query. For aggregated data across tenants, implement outside XAF's ObjectSpace.
User-Tenant Association
- Host DB: super admin accounts, tenant list, shared objects
- Tenant DB: tenant-specific users, roles, permissions, business data
// Create tenant user (run in tenant's ObjectSpace context)
var tenantUser = objectSpace.CreateObject<ApplicationUser>();
tenantUser.UserName = "newuser";
tenantUser.SetPassword("SecurePassword1!");
tenantUser.Roles.Add(defaultRole);
objectSpace.CommitChanges();
Admin Roles
| Role | DB | Responsibilities |
|---|---|---|
| Super Administrator | Host DB | Creates/deletes tenants, host config |
| Tenant Administrator | Tenant DB | Users, roles, permissions within tenant |
| Tenant User | Tenant DB | Normal end-user access |
// Host Updater.cs
var superAdminRole = objectSpace.CreateObject<PermissionPolicyRole>();
superAdminRole.Name = "SuperAdministrators";
superAdminRole.IsAdministrative = true;
// Tenant Updater.cs (runs per-tenant DB)
var tenantAdminRole = objectSpace.CreateObject<PermissionPolicyRole>();
tenantAdminRole.Name = "TenantAdministrators";
tenantAdminRole.IsAdministrative = true;
Modules in Multi-Tenant Apps
| Module | Host UI | Tenant UI |
|---|---|---|
| Business data modules | No | Yes |
| Dashboards | No | Yes |
| File Attachments | No | Yes |
| Reports V2 | No | Yes |
| Scheduler | No | Yes |
| Security module | Yes | Yes |
Modules that register persistent types cannot operate in Host UI mode.
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Singleton services holding tenant state | Use scoped services; never store tenant data in singletons |
| Same connection string for host and tenant | Always different connection strings — framework enforces this |
| Shared in-memory cache leaking across tenants | Use per-tenant cache keys: $"{tenantId}:{cacheKey}" |
| Querying business types in Host UI | Guard with objectSpace.CanInstantiate(typeof(T)) |
| Missing DB migration per tenant | Run migration per tenant DB via XAF's updater infrastructure |
// WRONG
services.AddSingleton<IMyService, MyService>(); // holds tenant-specific state
// CORRECT
services.AddScoped<IMyService, MyService>();
// CORRECT: per-tenant cache keys
string cacheKey = $"{tenantProvider.TenantId}:ProductList";
cache.Set(cacheKey, products);
Source Links
- Multi-tenancy: https://docs.devexpress.com/eXpressAppFramework/404436/multitenancy
- Create Multitenant Application: https://docs.devexpress.com/eXpressAppFramework/404436/multitenancy/create-a-multitenant-application
- Multi-Tenant Architecture: https://docs.devexpress.com/eXpressAppFramework/404436/multitenancy/multi-tenant-application-architecture
- Get Current Tenant in Code: https://docs.devexpress.com/eXpressAppFramework/404668/multitenancy/get-the-current-tenant-in-code
- ITenantProvider API: https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.MultiTenancy.ITenantProvider
More from kashiash/xaf-skills
xaf
DevExpress XAF (eXpressApp Framework) master index. Use this skill first when working with any XAF topic to find the right sub-skill. Covers Blazor and WinForms, EF Core and XPO, versions v24.2 and v25.1. Sub-skills: xaf-xpo-models, xaf-ef-models, xaf-controllers, xaf-editors, xaf-custom-editors, xaf-nonpersistent, xaf-security, xaf-multi-tenant, xaf-web-api, xaf-validation, xaf-reports, xaf-dashboards, xaf-office, xaf-blazor-ui, xaf-winforms-ui, xaf-conditional-appearance, xaf-deployment, xaf-memory-leaks.
13xaf-winforms-ui
XAF WinForms UI platform - WinApplication setup, Ribbon vs Standard toolbar, WinForms-specific editors (XtraGrid, DevExpress controls), Detail View layout customization via Layout Manager, custom WinForms controls embedded in XAF views, background workers for thread-safe UI updates, splash screen customization, WinForms navigation (NavigationFrame), printing/preview in WinForms, ClickOnce/MSI deployment. Use when building or customizing XAF WinForms applications.
10xaf-blazor-ui
XAF Blazor UI platform - BlazorApplication setup in Program.cs, AddXaf/AddXafBlazor services, InvokeAsync thread safety (critical for Blazor Server), async controller actions pattern, Blazor-specific editors, embedding custom Razor components as ViewItems using IComponentContentHolder, JavaScript interop via IJSRuntime, Detail View layout customization (tabs/groups), programmatic navigation, error handling with UserFriendlyException, SignalR configuration. Use when building or customizing XAF Blazor Server applications.
10xaf-office
XAF Office/Document Management Modules - FileAttachmentsModule with IFileData/FileAttachment patterns (XPO and EF Core), SpreadsheetModule for Excel editing with ISpreadsheetValueStorage, RichTextModule for Word-like editing with IRichTextDocumentProvider and mail merge, PdfViewerModule for PDF display, platform differences (Blazor vs WinForms), programmatic document manipulation. Use when adding file attachments, spreadsheet editing, rich text editing, or PDF viewing to DevExpress XAF applications.
9xaf-reports
XAF Reports Module (XtraReports v2) - ReportsModuleV2 setup for Blazor and WinForms, report storage (DB/filesystem/custom IReportStorageWebExtension), creating predefined reports in code (PredefinedReportsUpdater), data sources (CollectionDataSource/EntityServerModeSource), report parameters, programmatic export (PDF/Excel/Word), in-app designer, PrintAction, security permissions for report design vs view. Use when working with DevExpress XtraReports integration in XAF.
9xaf-editors
XAF built-in property editors and list editors - editor type mapping by data type, EditorAliases constants, [EditorAlias] attribute, [ModelDefault] for DisplayFormat/EditMask, ObjectPropertyEditor for inline sub-forms, list editor types (GridListEditor, DxGridListEditor, TreeListEditor, ChartListEditor), GridListEditor WinForms customization, DxGridListEditor Blazor customization, IModelListView/IModelColumn properties. Use when working with built-in XAF editors or customizing grid/list views.
8