orchardcore-tenants
Skill for configuring multi-tenancy in Orchard Core. Covers tenant creation, SaaS setup, tenant configuration, testing with tenants using different recipes (Blog, Agency), and tenant management APIs.
SKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-tenants/SKILL.md
GitHubOrchard Core Tenants - Prompt Templates
Configure Multi-Tenancy
You are an Orchard Core expert. Generate tenant configurations and multi-tenancy setup for Orchard Core.
Guidelines
- Orchard Core supports multi-tenancy natively; the SaaS setup recipe enables tenant management on the Default tenant.
- Each tenant is an isolated site sharing the same application instance.
- Tenants can have separate databases or share a database with table prefix isolation, or with a separate schema when the provider supports schemas.
- The
Defaulttenant is the main shell that manages other tenants. EnableOrchardCore.Tenantson it to manage tenants under Multi-Tenancy → Tenants (ManageTenantspermission). - Tenants can use any setup recipe (Blog, Agency, Blank, custom).
- URL prefixes or hostnames route requests to the correct tenant.
- A tenant name is also used as a folder name (for example
App_Data/Sites/{Tenant}and the tenant media root), so the admin UI, the Tenants API, and theCreateTenantTaskworkflow activity all validate it withITenantValidator. - Configure the module under
OrchardCore:Tenants(theOrchardCore_Tenantssection name is deprecated). - Other features:
OrchardCore.Tenants.FileProvider(per-tenant static files fromApp_Data/Sites/{Tenant}/wwwroot, e.g.robots.txt),OrchardCore.Tenants.FeatureProfiles, andOrchardCore.Tenants.Distributed(keeps tenant states in sync across instances; seeorchardcore-shells). - Always seal classes.
Enabling Multi-Tenancy
Use the SaaS recipe during setup to get tenant management out of the box, or enable manually:
{
"steps": [
{
"name": "Feature",
"enable": [
"OrchardCore.Tenants"
],
"disable": []
}
]
}
Setting Up with the SaaS Recipe
# Create a new project
dotnet new occms -n MyMultiTenantApp
cd MyMultiTenantApp
dotnet run
# During setup:
# - Choose the "SaaS" recipe
# - Use Sqlite for quick testing
# - Set admin credentials
After setup, navigate to Multi-Tenancy → Tenants in the admin panel to create and manage tenants.
Creating a Tenant via Admin UI
- Navigate to Multi-Tenancy → Tenants.
- Click Add Tenant.
- Set the Name (technical name, letters, digits and underscores; it becomes a folder name).
- Set the URL Prefix (e.g.,
blog) or URL Host (e.g.,blog.example.com). - Select a Recipe (Blog, Blank, etc.) and optionally a Feature Profile.
- Choose a Database Provider, and a Table Prefix or Schema if sharing a database (unless generated by a pattern).
- Click Create, then Setup to initialize the tenant.
The tenants list renders through the shared AdminList shape and supports bulk Enable, Disable, and (when allowed) Remove.
Creating a Tenant via Tenants API
The api/tenants endpoints use the Api authentication scheme (for example an OpenID bearer token) and require ManageTenants. Available actions are create, edit, setup, enable/{tenantName}, disable/{tenantName}, and remove/{tenantName}.
curl -X POST https://localhost:5001/api/tenants/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {{token}}" \
-d '{
"Name": "{{TenantName}}",
"RequestUrlPrefix": "{{UrlPrefix}}",
"Description": "{{Description}}",
"RecipeName": "Blog",
"DatabaseProvider": "Sqlite",
"FeatureProfiles": [],
"Category": ""
}'
Setting Up a Tenant Programmatically
Pass RecipeName for a harvested recipe, or Recipe with the recipe JSON; an inline recipe is written to the tenant-scoped temporary directory (ITempDirectoryProvider, OrchardCore:TempDirectory:Path) before it runs.
curl -X POST https://localhost:5001/api/tenants/setup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {{token}}" \
-d '{
"Name": "{{TenantName}}",
"SiteName": "{{SiteName}}",
"DatabaseProvider": "Sqlite",
"RecipeName": "Blog",
"UserName": "admin",
"Email": "admin@{{tenant}}.example.com",
"Password": "{{SecurePassword}}",
"SiteTimeZone": "UTC"
}'
Tenant States
ShellSettings.State is a TenantState: Uninitialized (created, not set up), Initializing (setup running), Running, Disabled (set up but turned off), or Invalid. Check it with IsUninitialized(), IsInitializing(), IsRunning(), IsDisabled(), IsInitialized() (running or disabled), IsRemovable() (uninitialized or disabled) and IsDefaultShell(); set it with AsUninitialized(), AsRunning(), AsDisabled(). All live in OrchardCore.Environment.Shell.
Creating a Tenant in Code
This is what the admin Create action does. Run it on the Default tenant with OrchardCore.Tenants enabled, and validate the input with ITenantValidator (namespace OrchardCore.Tenants.Services) first when it comes from users.
using OrchardCore.Environment.Shell;
public sealed class TenantProvisioner
{
private readonly IShellHost _shellHost;
private readonly IShellSettingsManager _shellSettingsManager;
public TenantProvisioner(IShellHost shellHost, IShellSettingsManager shellSettingsManager)
{
_shellHost = shellHost;
_shellSettingsManager = shellSettingsManager;
}
public async Task CreateAsync(string name, string urlPrefix)
{
// Seeded from configuration; dispose it once the shell host has its own copy.
using var shellSettings = _shellSettingsManager
.CreateDefaultSettings()
.AsUninitialized()
.AsDisposable();
shellSettings.Name = name;
shellSettings.RequestUrlPrefix = urlPrefix;
shellSettings.RequestUrlHost = string.Empty;
shellSettings["Description"] = $"{name} site";
shellSettings["DatabaseProvider"] = "Sqlite";
shellSettings["RecipeName"] = "Blog";
shellSettings["FeatureProfile"] = "standard";
shellSettings["Secret"] = Guid.NewGuid().ToString();
// Stamps a new VersionId, saves the settings and reloads the shell.
await _shellHost.UpdateShellSettingsAsync(shellSettings);
}
}
| Key | Meaning |
|---|---|
DatabaseProvider, ConnectionString, TablePrefix, Schema |
Database settings applied at setup. |
RecipeName |
Setup recipe preselected for the tenant. |
FeatureProfile |
Comma-separated feature profile names. |
Secret |
Token secret used for the setup link. |
Category, Description |
Shown and filterable in the tenants list. |
The new tenant is Uninitialized; it still needs setup (database schema, admin user, recipe). Use Setup in the tenants list, POST api/tenants/setup, or ISetupService.SetupAsync(SetupContext) with SetupConstants property keys (SiteName, AdminUsername, AdminEmail, AdminPassword, SiteTimeZone, DatabaseProvider, DatabaseConnectionString, DatabaseTablePrefix, DatabaseSchema), which is what the API action does.
Creating Tenants from Workflows
With OrchardCore.Workflows enabled, the tenant activities are Create Tenant Task, Setup Tenant Task, Enable Tenant Task, and Disable Tenant Task. CreateTenantTask runs the same ITenantValidator validation as the admin UI and API: an invalid name, a duplicate URL prefix and host, or an unknown feature profile takes the Failed outcome and the validation errors are assigned to LastResult. On success it takes Done with the new ShellSettings in LastResult.
Testing with Blog Tenant
# 1. Start the application and set up the Default shell with the SaaS recipe.
dotnet run
# 2. Create a "Blog" tenant in Multi-Tenancy > Tenants:
# - Name: Blog
# - URL Prefix: blog
# - Recipe: Blog
# - Database: Sqlite
# 3. Set up the blog tenant, then browse https://localhost:5001/blog
Tenant Configuration in appsettings.json
Each tenant can be configured under a section named after it:
{
"OrchardCore": {
"Default": {
"State": "Running",
"RequestUrlHost": "",
"RequestUrlPrefix": ""
},
"{{TenantName}}": {
"State": "Running",
"RequestUrlHost": "",
"RequestUrlPrefix": "{{UrlPrefix}}",
"DatabaseProvider": "Sqlite",
"ConnectionString": "",
"TablePrefix": "{{TablePrefix}}",
"Schema": "{{Schema}}"
}
}
}
Tenants Module Options
Configure TenantsOptions under OrchardCore:Tenants:
{
"OrchardCore": {
"Tenants": {
"TenantRemovalAllowed": false,
"RequireTablePrefix": true,
"TablePrefixPattern": "{{ ShellSettings.Name }}",
"SchemaPattern": "dbo"
}
}
}
TenantRemovalAllowedallows removing a tenant that is not set up yet or is disabled (defaultfalse).RequireTablePrefixrequires a non-empty table prefix when creating a tenant or editing an uninitialized one, for providers that support prefixes.TablePrefixPatternandSchemaPatternare Fluid templates withShellSettingsin scope. A pattern generates the value and removes the manual field from the tenant UI. Generated and manual values must be valid SQL identifiers (letters, digits, underscores; starting with a letter or underscore).- The built-in SQLite provider does not share a single database file between tenants through these patterns.
When OrchardCore:DatabaseProvider is configured, the provider is fixed and is not selectable in the Setup or Tenants UI. When both OrchardCore:DatabaseProvider and OrchardCore:ConnectionString are configured, the connection string is fixed as well. Setup asks for a connection string only when the selected provider requires one and the tenant has no saved value.
Feature Profiles for Tenants
Enable OrchardCore.Tenants.FeatureProfiles on the Default tenant, then manage profiles under Multi-Tenancy → Feature Profiles (ManageTenantFeatureProfiles). Rules are applied in order; Exclude and Include accept * and ? wildcards. Select a profile when creating or editing a tenant, through the API (FeatureProfiles), or in Auto Setup.
{
"steps": [
{
"name": "Feature",
"enable": [
"OrchardCore.Tenants",
"OrchardCore.Tenants.FeatureProfiles"
],
"disable": []
},
{
"name": "FeatureProfiles",
"FeatureProfiles": {
"standard": {
"FeatureRules": [
{
"Rule": "Exclude",
"Expression": "OrchardCore.Tenants*"
},
{
"Rule": "Exclude",
"Expression": "OrchardCore.MiniProfiler"
}
]
}
}
}
]
}
The All Feature Profiles deployment step exports the profiles.
Accessing Tenant Information in Code
using OrchardCore.Environment.Shell;
public sealed class TenantInfoService
{
private readonly ShellSettings _shellSettings;
public TenantInfoService(ShellSettings shellSettings)
{
_shellSettings = shellSettings;
}
public string GetCurrentTenantName()
{
return _shellSettings.Name;
}
public string GetRequestUrlPrefix()
{
return _shellSettings.RequestUrlPrefix;
}
}
Running Tenant-Specific Code
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Scope;
public sealed class TenantOperationService
{
private readonly IShellHost _shellHost;
public TenantOperationService(IShellHost shellHost)
{
_shellHost = shellHost;
}
public async Task RunInTenantAsync(string tenantName, Func<ShellScope, Task> action)
{
var shellScope = await _shellHost.GetScopeAsync(tenantName);
await shellScope.UsingAsync(action);
}
public async Task RunInAllRunningTenantsAsync(Func<ShellScope, Task> action)
{
foreach (var settings in _shellHost.GetAllSettings().Where(settings => settings.IsRunning()))
{
var (scope, success) = await _shellHost.TryGetScopeAsync(settings.Name);
if (success)
{
await scope.UsingAsync(action);
}
}
}
}
Inside the delegate, resolve services from scope.ServiceProvider (or ShellScope.Services); they belong to that tenant. From code already running in a tenant, ShellScope.UsingChildScopeAsync(tenantName, async scope => { ... }) does the same. Never resolve tenant services such as IContentManager or ISession from the application root provider. Scope mechanics are covered in orchardcore-shells.
Gotchas
- Creating settings is not a usable site; a new tenant must be set up before it serves requests.
- Dispose the
ShellSettingscreated byCreateDefaultSettings()(.AsDisposable()plususing). - Only the Default tenant (
ShellSettings.DefaultShellName) manages other tenants. - Every module
Startupruns per tenant, so a singleton is one instance per tenant, not per application.
Upgrading from 3.x
- Rename the
OrchardCore_Tenantsconfiguration section toOrchardCore:Tenants. CreateTenantTasknow validates its input withITenantValidator; workflows that created tenants with invalid names, duplicate prefix and host, or missing feature profiles now takeFailed. Its constructor gained anITenantValidatorparameter, so code that constructs it directly must pass one.- Tenant recipe uploads use
ITempDirectoryProviderinstead ofPath.GetTempPath(). - A theme or module that overrides the tenants list view must render the
Listcontext with the<admin-list>tag helper (seeorchardcore-admin-lists).