name: orchardcore-tenants description: Works with OrchardCore multi-tenancy — shells, ShellSettings, creating/configuring tenants programmatically, running code in a tenant scope, feature profiles, and data isolation. Use when the user needs to create or manage tenants, run code inside a tenant, pre-configure tenants, or understand shell lifecycle.
OrchardCore Tenants (Multi-tenancy)
This skill guides you through OrchardCore multi-tenancy following project conventions.
OrchardCore runs many isolated sites ("tenants") in one process. Each tenant = a shell: its own DI container, configuration, enabled features, and (optionally) its own database or table prefix. The Default tenant is special — it's created first and manages the others.
Core concepts
| Type | Role |
|---|---|
ShellSettings |
a tenant's identity + config (Name, State, RequestUrlHost, RequestUrlPrefix, DB info) |
ShellContext |
a running tenant: its ServiceProvider, blueprint, request pipeline |
ShellScope |
a unit-of-work scope inside a tenant (resolve services here) |
IShellHost |
manages all shells: create, reload, list, get scopes |
IShellSettingsManager |
persists ShellSettings |
Tenant states
Uninitialized → (setup) → Running. Also Initializing, Disabled, Invalid. A tenant configured but not yet set up is Uninitialized and appears in the Tenants list awaiting setup.
Decide what you're doing
| Goal | Approach |
|---|---|
| Create a tenant in code | IShellSettingsManager.CreateDefaultSettings() + IShellHost.UpdateShellSettingsAsync |
| Run code inside a tenant | IShellHost.GetScopeAsync(name) + scope.UsingAsync(...) |
| Pre-declare tenants without UI | appsettings/configuration tenant section |
| Restrict a tenant's features | Feature Profiles |
| Isolate tenant data | separate connection string, or shared DB + table prefix/schema |
Workflow A: create a tenant programmatically
using var shellSettings = _shellSettingsManager
.CreateDefaultSettings()
.AsUninitialized()
.AsDisposable();
shellSettings.Name = "acme";
shellSettings.RequestUrlPrefix = "acme"; // site at /acme
shellSettings.RequestUrlHost = ""; // or a host header
shellSettings["ConnectionString"] = connectionString;
shellSettings["DatabaseProvider"] = "Sqlite";
shellSettings["TablePrefix"] = "acme";
shellSettings["RecipeName"] = "Blog"; // recipe run at setup
shellSettings["Secret"] = Guid.NewGuid().ToString();
await _shellHost.UpdateShellSettingsAsync(shellSettings);
UpdateShellSettingsAsync stamps a new VersionId, saves, and reloads the shell. The tenant is now Uninitialized — it still needs setup (admin user, run recipe), done via the Setup UI/API or ISetupService.
Or via the Tenants API: POST /api/tenants/create then POST /api/tenants/setup.
Workflow B: run code in a tenant scope
To resolve tenant-scoped services (content manager, session, etc.) for a specific tenant:
var shellScope = await _shellHost.GetScopeAsync("acme");
await shellScope.UsingAsync(async scope =>
{
var contentManager = scope.ServiceProvider.GetRequiredService<IContentManager>();
// ... work inside the "acme" tenant ...
});
Inside the callback, ShellScope.Services / ShellScope.Current resolve to this tenant. Deferred tasks run on scope disposal. Never resolve tenant services off the root provider — you'll get the wrong (or no) tenant.
Workflow C: pre-configure tenants via configuration
In appsettings.json (or any config source), declare a tenant so it exists before any UI interaction:
{
"OrchardCore": {
"acme": {
"State": "Uninitialized",
"RequestUrlPrefix": "acme",
"ConnectionString": "...",
"DatabaseProvider": "SqlConnection"
}
}
}
Per-tenant module config goes under the tenant name too:
{ "OrchardCore": { "Default": { "OrchardCore_Media": { /* ... */ } } } }
Quick Reference
IShellHost
| Member | Purpose |
|---|---|
GetOrCreateShellContextAsync(settings) |
get/build a tenant's context |
GetScopeAsync(settings|name) |
a scope to run code in |
UpdateShellSettingsAsync(settings) |
save + reload a tenant |
ReloadShellContextAsync(settings) |
rebuild after config change |
ListShellContexts() / TryGetSettings(name, out) |
enumerate / look up |
ShellSettings state extensions
AsUninitialized(), AsRunning(), AsDisabled(), plus IsRunning(), IsUninitialized(), etc.
Settings keys (bag)
ConnectionString, DatabaseProvider, TablePrefix, Schema, RecipeName, FeatureProfile, Secret, Category, Description.
Data isolation options
| Strategy | How |
|---|---|
| Separate database | distinct ConnectionString per tenant |
| Shared DB, table prefix | same DB + unique TablePrefix |
| Shared DB, schema | Schema per tenant (SQL Server) |
OrchardCore_Tenants config can enforce: RequireTablePrefix, TablePrefixPattern, SchemaPattern (templated with ShellSettings).
Feature Profiles
Feature OrchardCore.Tenants.FeatureProfiles. Profiles are include/exclude rules limiting which features a tenant may enable:
{
"name": "FeatureProfiles",
"FeatureProfiles": {
"minimal": { "FeatureRules": [ { "Rule": "Exclude", "Expression": "OrchardCore.MiniProfiler" } ] }
}
}
Assign via shellSettings["FeatureProfile"].
Gotchas
- Creating settings ≠ a usable site. A new tenant is
Uninitialized; it must be set up (recipe + admin user) before it runs. - Always
using/dispose theShellSettingsfromCreateDefaultSettings()(.AsDisposable()). - Only the
Defaulttenant (ShellSettings.DefaultShellName) can manage other tenants. - Resolve services through a
ShellScope, not the application root provider — tenant DI is a child container per shell. - Each tenant runs each module's
Startup.ConfigureServicesindependently; services are per-tenant singletons, not app-wide.
References
references/shells.md— shell lifecycle, scope mechanics, container factory, config sourcessrc/docs/reference/modules/Tenants/README.md(repo)src/docs/reference/modules/Configuration/README.md(repo)src/docs/reference/modules/Shells/README.md(repo)AGENTS.md(repo root) — build commands