xaf-create-multitenant-application
xaf-create-multitenant-application copied to clipboard
This example creates a multi-tenant XAF application.
XAF - How to Create a Multi-Tenant Application
XAF v23.2 marks the first official release of the DevExpress Multi-Tenancy Module. This release supports straightforward CRUD usage scenarios and includes the following built-in features:
- EF Core and XPO ORM support
- Authentication: Log in with an email/OAuth2 account (like Microsoft Entra ID or Google) and a password (the domain automatically resolves the tenant and its storage).
- Tenant Isolation: Multi-tenant app with multiple databases (a database per tenant).
- Database Creation: The application automatically creates a tenant database and schema at runtime (if the database does not exist).
This example application is a modern multi-tenant iteration of our original WinForms-based Outlook Inspired App. It serves as the central data management hub for the fictitious company, overseeing various business entities such as Employees, Products, Orders, Quotes, Customers, and Stores.

For additional information, refer to the Multitenancy section of our online documentation.
Before you review this XAF sample project, please take a moment to complete a short multi-tenancy related survey (share your multi-tenancy requirements with us).
Run the Application
When you launch the WinForms or Blazor application for the first time, you can login using the "Admin" account and a blank password. The application will execute in Host User Interface mode (used to view, create and edit Tenants).

Once you log in, two tenants are created in the system: company1.com and company2.com. You can view the tenant list in the Host User Interface List View.
After the Host Database is initialized, you can log in to the Tenant User Interface using one of the following Tenant Administrator accounts
A Tenant Administrator has full access to all data stored in the Tenant Database but no access to other Tenant data. Users and permissions are managed in each tenant independently.

In addition, the sample application creates a list of users with restricted access rights in each tenant (for example [email protected], [email protected] and others).
Documentation | Getting Started | Best Practices and Limitations | Modules in a Multi-Tenant Application
Implementation Details
Enable Multi-Tenancy
In the Blazor application, the following code activates multi-tenancy.
OutlookInspired.Blazor.Server/Services/Internal/ApplicationBuilder.cs:
public static IBlazorApplicationBuilder AddMultiTenancy(this IBlazorApplicationBuilder builder, IConfiguration configuration){
builder.AddMultiTenancy()
.WithHostDbContext((_, options) => {
#if EASYTEST
string connectionString = configuration.GetConnectionString("EasyTestConnectionString");
#else
string connectionString = configuration.GetConnectionString("ConnectionString");
#endif
options.UseSqlServer(connectionString);
options.UseChangeTrackingProxies();
options.UseLazyLoadingProxies();
})
.WithMultiTenancyModelDifferenceStore(e => {
#if !RELEASE
e.UseTenantSpecificModel = false;
#endif
})
.WithTenantResolver<TenantByEmailResolver>();
return builder;
}
In the WinForms application, the following code activates multi-tenancy.
OutlookInspired.Win/Services/ApplicationBuilder.cs:
public static IWinApplicationBuilder AddMultiTenancy(this IWinApplicationBuilder builder, string serviceConnectionString) {
builder.AddMultiTenancy()
.WithHostDbContext((_, options) => {
options.UseSqlServer(serviceConnectionString);
options.UseChangeTrackingProxies();
options.UseLazyLoadingProxies();
})
.WithMultiTenancyModelDifferenceStore(mds => {
#if !RELEASE
mds.UseTenantSpecificModel = false;
#endif
})
.WithTenantResolver<TenantByEmailResolver>();
return builder;
}
Configure ObjectSpaceProviders for Tenants
In the Blazor application:
OutlookInspired.Blazor.Server/Services/Internal/ApplicationBuilder.cs:
// ...
builder.WithDbContext<Module.BusinessObjects.OutlookInspiredEFCoreDbContext>((serviceProvider, options) => {
// ...
options.UseSqlServer(serviceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString());
})
// ...
In the WinForms application.
OutlookInspired.Win/Services/ApplicationBuilder.cs:
// ...
builder.WithDbContext<OutlookInspiredEFCoreDbContext>((application, options) => {
// ...
options.UseSqlServer(application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString());
}, ServiceLifetime.Transient)
// ...
Populate Databases with Data
A multi-tenant application works with several independent databases:
- Host database – stores a list of Super Administrators and the list of tenants.
- One or multiple tenant databases – store user data independently from other organizations (tenants).
A Tenant database is created and populated with demo data on the first login to the tenant itself.
A list of the tenants is created, and tenant databases are populated with demo data in the Module Updater:
OutlookInspired.Module/DatabaseUpdate/Updater.cs:
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
if (ObjectSpace.TenantName() == null) {
CreateAdminObjects();
CreateTenant("company1.com", "OutlookInspired_company1");
CreateTenant("company2.com", "OutlookInspired_company2");
ObjectSpace.CommitChanges();
}
// ...
}
private void CreateTenant(string tenantName, string databaseName) {
var tenant = ObjectSpace.FirstOrDefault<Tenant>(t => t.Name == tenantName);
if (tenant == null) {
tenant = ObjectSpace.CreateObject<Tenant>();
tenant.Name = tenantName;
tenant.ConnectionString = $"Integrated Security=SSPI;MultipleActiveResultSets=True;Data Source=(localdb)\\mssqllocaldb;Initial Catalog={databaseName}";
}
}
To determine the tenant whose database is being updated when the Module Updater executes, the Updater class includes the TenantId and TenantName properties that return the current tenant's unique identifier and name respectively. When the Host Database is updated, the tenant is not specified, and these properties return null.
Solution Overview
Domain Diagram
The following diagram describes the application's domain architecture:

Solution Structure
The solution consists of three distinct projects.
- OutlookInspired.Module - A platform-agnostic module required by all other projects.
- OutlookInspired.Blazor.Server - A Blazor port of the original OutlookInspired demo.
- OutlookInspired.Win - A WinForms port of the original OutlookInspired demo.
OutlookInspired.Module project
Services Folder
This folder serves as the centralized storage for app business logic so that all other class implementations can be compact. For instance, methods that utilize XafApplication are located in Services/Internal/XafApplicationExtensions:
public static IObjectSpace NewObjectSpace(this XafApplication application)
=> application.CreateObjectSpace(typeof(OutlookInspiredBaseObject));
Methods that use IObjectSpace can be found in Services/Internal/ObjectSpaceExtensions. For example:
public static TUser CurrentUser<TUser>(this IObjectSpace objectSpace) where TUser:ISecurityUser
=> objectSpace.GetObjectByKey<TUser>(objectSpace.ServiceProvider.GetRequiredService<ISecurityStrategyBase>().UserId);
The SecurityExtensions class configures a diverse set of permissions for each department. For instance, the Management department will have:
- CRUD permissions for
EmployeeTypes - Read-only permissions for
CustomerTypes - Navigation permissions for
Employees,Evaluations, andCustomers - Mail merge permissions for orders and customers
- Permissions for various reports including
Revenue,Contacts,TopSalesMan, andLocations.
Services/Internal/ObjectSpaceExtensions:
private static void AddManagementPermissions(this PermissionPolicyRole role)
=> EmployeeTypes.AddCRUDAccess(role)
.Concat(CustomerTypes.Prepend(typeof(ApplicationUser)).AddReadAccess(role)).To<string>()
.Concat(new[]{ EmployeeListView,EvaluationListView,CustomerListView}.AddNavigationAccess(role))
.Finally(() => {
role.AddMailMergePermission(data => new[]{ MailMergeOrder, MailMergeOrderItem, ServiceExcellence }.Contains(data.Name));
role.AddReportPermission(data => new[]{ RevenueReport, Contacts, LocationsReport, TopSalesPerson }.Contains(data.DisplayName));
})
.Enumerate();
Attributes Folder
The Attributes folder contains attribute declarations.
-
FontSizeDeltaAttributeThis attribute is applied to properties ofCustomer,Employee,Evaluation,EmployeeTask,Order, andProducttypes to configure font size. he implementation is context-dependent; in the WinForms application, this attribute it is used by theLabelPropertyEditor...OutlookInspired.Win/Editors/LabelControlPropertyEditor.cs:
protected override object CreateControlCore() => new LabelControl{ BorderStyle = BorderStyles.NoBorder, AutoSizeMode = LabelAutoSizeMode.None, ShowLineShadow = false, Appearance ={ FontSizeDelta = MemberInfo.FindAttribute<FontSizeDeltaAttribute>()?.Delta??0, TextOptions = { WordWrap =MemberInfo.Size==-1? WordWrap.Wrap:WordWrap.Default} } };... and the
GridView.OutlookInspired.Win/Services/Internal/Extensions.cs:
public static void IncreaseFontSize(this GridView gridView, ITypeInfo typeInfo){ var columns = typeInfo.AttributedMembers<FontSizeDeltaAttribute>().ToDictionary( attribute => gridView.Columns[attribute.memberInfo.BindingName].VisibleIndex, attribute => attribute.attribute.Delta); gridView.CustomDrawCell += (_, e) => { if (columns.TryGetValue(e.Column.VisibleIndex, out var column)) e.DrawCell( column); }; }
In the Blazor application,
FontSizeDeltaAttributedependent logic is implemented in the following extension method.OutlookInspired.Blazor.Server/Services/Internal/Extensions.cs:
public static string FontSize(this IMemberInfo info){ var fontSizeDeltaAttribute = info.FindAttribute<FontSizeDeltaAttribute>(); return fontSizeDeltaAttribute != null ? $"font-size: {(fontSizeDeltaAttribute.Delta == 8 ? "1.8" : "1.2")}rem" : null; }The attribute is used like its WinForms application counterpart:

-
Appearance Subfolder
The following Conditional Appearance module attributes are in this subfolder:
DeactivateActionAttribute: This is an extension of the Conditional Appearance module used to deactivate actions.Attributes/Appearance/DeactivateActionAttribute.cs:
puOutlookInspired.Blazor.Server/Services/Internal/Extensions.csbute { public DeactivateActionAttribute(params string[] actions) : base($"Deactivate {string.Join(" ", actions)}", DevExpress. ExpressApp.ConditionalAppearance.AppearanceItemType.Action, "1=1") { Visibility = ViewItemVisibility.Hide; TargetItems = string.Join(";", actions); } }In much the same way, we derive from this attribute to create other attributes found in the same folder (
ForbidCRUDAttribute,ForbidDeleteAttribute,ForbidDeleteAttribute). -
Validation Subfolder
This folder includes attributes that extend the XAF Validation module. Available attributes include:
EmailAddressAttribute,PhoneAttribute,UrlAttribute,ZipCodeAttribute. The following code snippet illustrates how theZipCodeAttributeis implemented. Other attributes are implemented in a similar fashion.Attributes/FontSizeDeltaAttribute.cs:
public class ZipCodeAttribute : RuleRegularExpressionAttribute { public ZipCodeAttribute() : base(@"^[0-9][0-9][0-9][0-9][0-9]$") { CustomMessageTemplate = "Not a valid ZIP code."; } }
Controllers Folder
This folder contains controllers with no dependencies:
- The
HideToolBarController- extends the XAFIModelListViewinterface with aHideToolBarattribute so we can hide the nested list view toolbar. - The
SplitterPositionController- extends the XAF model with aRelativePositionproperty used to configure the splitter position.
Features Folder
This folder implements features specific to the solution.
-
CloneView Subfolder
This subfolder contains the CloneViewAttribute declaration, used to generate views (in addition to default views). For example:
[CloneView(CloneViewType.DetailView, LayoutViewDetailView)] [CloneView(CloneViewType.DetailView, ChildDetailView)] [CloneView(CloneViewType.DetailView, MapsDetailView)] [VisibleInReports(true)] [ForbidDelete()] public class Employee : OutlookInspiredBaseObject, IViewFilter, IObjectSpaceLink, IResource, ITravelModeMapsMarker { public const string MapsDetailView = "Employee_DetailView_Maps"; public const string ChildDetailView = "Employee_DetailView_Child"; public const string LayoutViewDetailView = "EmployeeLayoutView_DetailView"; // ... } -
Customers Subfolder
This subfolder includes Customer-related controllers, such as:
-
MailMergeController
XAF ships with built-in mail merge support. This controller modifies the default
ShowInDocumentActionicons.
-
ReportsController
This controller declares an action used to display Customer Reports. (The XAF Reports module API is used).

-
-
Employees Subfolder
This subfolder includes Employee-related controllers such as:
-
RoutePointController
This controller sets travel distance (calculated using the MAP service).
WindowsForms:

Blazor:

-
-
Maps Subfolder
This subfolder includes mapping-related logic, including:
-
MapsViewController
This controller declares map-related actions (
MapItAction,TravelModeAction,ExportMapAction,PrintPreviewMapAction,PrintAction,StageAction,SalesPeriodAction) and manages associated state based onISalesMapMarkerandIRoutePointMapMarkerinterfaces.
-
-
MasterDetail Subfolder
This subfolder adds platform-agnostic master-detail capabilities based on XAF's DashboardViews.
-
MasterDetailController, IUserControl
TheIUserControlis implemented in a manner similar to the technique described in the following topic: How to: Include a Custom UI Control That Is Not Integrated by Default (WinForms, ASP.NET WebForms, and ASP.NET Core Blazor). The distinction lies in the addition ofUserControl(for WinForms) and the Component (for Blazor) to aDetailView.
-
-
Orders Subfolder
This subfolder includes functionality specific to the Sales moduel.
-
FollowUpController
Declares an action used to display the follow-up mail merge template for the selected order.

-
InvoiceController
Uses a master-detail mail merge template pair to generate an invoice, converts it to a PDF, and displays it using the
PdfViewEditor. -
Pay/Refund Controllers
These controllers declare actions used to mark the selected order as either Paid or Refunded.
-
ReportController
Provides access to Order Revenue reports.

-
ShipmentDetailController
Adds a watermark to the Shipment Report based on order status.

-
-
Products Subfolder
This subfolder includes functionality specific to the Products module.
-
ReportsController
Declares an action used to display reports for Sales, Shipments, Comparisons, and Top Sales Person.

-
-
Quotes Subfolder
This subfolder includes functionality specific to the Quotes module.
-
QuoteMapItemController
Calculates non-persistent
QuoteMapItemobjects used by the Opportunities view
-
-
ViewFilter Subfolder
This subfolder includes our implementation of a Filter manager, used by the end-user to create and save view filters.

OutlookInspired.Win project
This is a WinForms frontend project. It utilizes the previously mentioned OutlookInspired.Module and adheres to the same folder structure.
Controllers Folder
This folder contains the following controllers with no dependencies:
-
DisableSkinsController- This controller disables the XAF default theme-switching action. -
SplitterPositionController- This is the WinForms implementation of the SplitterPositionController. We discussed its platform agnostic counterpart in theOutlookInspired.Modulesection.
Editors Folder
This folder contains custom controls and XAF property editors.
-
ColumnViewUserControl- This is a base control that implements IUserControl discussed previously. -
EnumPropertyEditor- This is a subclass of the built-inEnumPropertyEditor(it only displays an image).
-
HyperLinkPropertyEditor- This editor displays hyperlinks with mailto support.
-
LabelControlPropertyEditor- This is an editor that renders a label.
-
PdfViewerEditor- This is a PDF viewer based on the DevExpress PDF Viewer component.
-
PrintLayoutRichTextEditor- This editor extends the built-inRichTextPropertyEditor, but uses thePrintLayoutmode. -
ProgressPropertyEditor- This editor is used to display progress across various contexts.
Services Folder
Much like the platform-agnostic module's Services Folder, our WinForms project keeps all classes as small as possible and implements business logic in extension methods.
Features Folder
This folder contains custom functionality specific to the solution.
-
Maps Subfolder
This subfolder includes logic related to mapping.
-
MapsViewController - This controller overrides the platform-agnostic
MapsViewControllerto further configure the state of map actions. -
WinMapsViewController - This is an abstract controller that provides functionality used by its derived classes -
SalesMapsViewControllerandRouteMapsViewController. The controller configures Map views for all objects that implementISalesMapsMarker(Customer, Product) andIRouteMapsMarker(Order, Employee) interfaces.


-
-
Customers Subfolder
This subfolder contains customer module-related functionality.
- CustomerGridView, CustomerLayoutView, and CustomerStoreView: These classes derive from the previously discussed
ColumnViewUserControl. They host customGridControlvariants, such as master-detail layouts.

- CustomerGridView, CustomerLayoutView, and CustomerStoreView: These classes derive from the previously discussed
-
Employees Subfolder
This subfolder contains employee module-related functionality.
- EmployeesLayoutView - This is a descendant of
ColumnViewUserControlthat hosts a GridControl LayoutView.

- EmployeesLayoutView - This is a descendant of
-
GridListEditor Subfolder
This subfolder contains functionality related to the default XAF GridListEditor.
-
FontSizeController- Uses theFontSizeDeltadiscussed in the platfrom-agnostic module section to increase font size in row cells of an AdvancedBanded Grid. -
NewItemRowHandlingModeController- Modifies how new object are handled when a dashboard master detail view (discussed in the platform-agnostic module section) objects are created.
-
-
Products Subfolder
This subfolder contains functionality related to products.
-
ProductCardView- This is a descendant ofColumnViewUserControlthat hosts a GridControl LayoutView.
-
-
Quotes Subfolder
This subfolder contains opportunity module-related functionality.
-
WinMapsController,PaletteEntriesController- Configures the opportunities maps view.
-
FunnelFilterController- Filters the Funnel chart when the FilterManager discussed in the platform-agnostic module section is executed. -
PropertyEditorController- Assigns progress to the Pivot cell.
-
OutlookInspired.Blazor.Server Project
This is the Blazor frontend project. It utilizes the previously mentioned OutlookInspired.Module and maintains the same folder structure.
Components Folder
This folder contains Blazor components essential for project requirements.
-
ComponentBase, ComponentModelBase -
ComponentBaseis the foundation for client-side components like DxMap, DxFunnel, DXPivot, and PdfViewer. It manages loading of resources such as JavaScript files.ComponentModelBaseacts as the base model for all components, offering functionality such asClientReadyevent and a hook for browser console messages, among other features. -
HyperLink, Label - TThese components mirror their WinForms counterparts and are used to render hyperlinks and labels.

-
PdfViewer - This is a
ComponentBasedescendant used to view PDF files.
-
XafImg, BOImage - Both components are used to display images across a variety of contexts.

-
XafChart - This component is utilized for charting Customer store data.

-
CardView Subfolder
This folder contains the
SideBySideCardViewand theStackedCardView. They are used to display Card like list views as follows:
-
DevExtreme Subfolder
This folder includes reusable .NET components, including Map, VectorMap, Funnel and Chart DevExtreme Widgets.
Controllers Folder
This folder contains the following controllers with no dependencies:
CellDisplayTemplateController- Is an abstract controller that allows the application to render GridListEditor row cell fragments.DxGridListEditorController- Overiddes GridListEditor behaviors (such as removing command columns).PopupWindowSizeController- Configures the size of popup windows.
Editors Folder
This folder contains XAF custom editors. Examples include:
-
ChartListEditor- An abstract list editor designed to create simple object-specific variants.[ListEditor(typeof(MapItem), true)] public class MapItemChartListEditor : ChartListEditor<MapItem, string, decimal, string, XafChart<MapItem, string, decimal, string>> { public MapItemChartListEditor(IModelListView info) : base(info) { } } -
ComponentPropertyEditor- An abstract property editor that serves as a basis for editors such asProgressPropertyEditororPdfViewEditor. The latter uses the PdfViewer component from the Components folder.Editors/ComponentPropertyEditor.cs:
[PropertyEditor(typeof(byte[]), EditorAliases.PdfViewerEditor)] public class PdfViewerEditor : ComponentPropertyEditor<PdfModel, PdfModelAdapter, byte[]> { public PdfViewerEditor(Type objectType, IModelMemberViewItem model) : base(objectType, model) { } } -
EnumPropertyEditor- Inherits from XAF's native EnumPropertyEditor, but only displays an image (like its WinForms counterpart). -
DisplayTestPropertyEditors- Displays raw text (like the WinForms LabelPropertyEditor).
Features Folder
This folder contains solution-specific functionality.
-
Customers subfolder
Uses components from
Components(bound to data) to render customer-related data. For example, it uses theStackedCardViewwith aStackedInfoCardas shown below:Features/Customers/Stores/StoresCardView.razor:
<StackedCardView> <Content> @foreach (var store in ComponentModel.Stores){ <StackedInfoCard Body="@store.City" Image="@store.Crest.LargeImage.ToBase64Image()"/> } </Content>
The visual output is as follows:

-
Employees subfolder
Uses data-bound components from the
Componentsfolder to render employee-related data.Features/Employees/CardView/CardView.razor:
<StackedCardView > <Content> @foreach (var employee in ComponentModel.Objects){ <SideBySideInfoCard CurrentObject="employee" ComponentModel="@ComponentModel" Image="@employee.Picture?.Data?.ToBase64Image()" HeaderText="@employee.FullName" InfoItems="@(new Dictionary<string, string>{{ "ADDRESS", employee.Address }, { "EMAIL", $"<a href=\"mailto:{employee.Email}\">{employee.Email}</a>" },{ "PHONE", employee.HomePhone } })"/> } </Content>
Results are as follows:

The Evaluations and Tasks include components responsible for rendering the cell fragment in the following image. Both components are linked to the cell through Controllers\CellDisplayTemplateController.

-
Evaluations subfolder
The
SchedulerGroupTypeControlleris required to set up the scheduler, as follows:
-
Maps subfolder
Mirroring its WinForms counterpart, this subfolder contains both the
RouteMapsViewControllerand theSalesMapsViewController. These controllers are needed to configure maps (ModalDxMapandModalDxVectorMap) and associated actions (such asTravelMode,SalesPeriod,Print, etc). Components within this directory are fragments that use components inComponents/DevExtreme. Additionally, they adjust height as they are displayed in a modal popup window. -
Orders subfolder
The
DetailRowcomponent renders the detail fragment for theOrderListView.
-
Products subfolder
Much like the Employees subfolder, the
Component/CardViews/StackedCardViewdeclaration is as follow:Features/Products/CardView.razor:
<StackedCardView> <Content> @foreach (var product in ComponentModel.Objects) { <SideBySideInfoCard CurrentObject="product" ComponentModel="@ComponentModel" Image="@product.PrimaryImage.Data.ToBase64Image()" HeaderText="@product.Name" InfoItems="@(new Dictionary<string, string>{{ "COST", product.Cost.ToString("C") }, { "SALE PRICE", product.SalePrice.ToString("C") } })" FooterText="@product.Description.ToDocument(server => server.Text)"/> } </Content> </StackedCardView>Results are as follows:
