fullstackhero/dotnet-starter-kit · error · CustomException
The root tenant cannot be deactivated.
Error message
The root tenant cannot be deactivated.
What it means
DeactivateAsync explicitly forbids deactivating the root tenant (MultitenancyConstants.Root.Id, compared case-insensitively). The root tenancy is the host-level tenant that other tenants depend on, so it must remain active. This check runs after the already-deactivated and active-count guards.
Solutions
- Exclude the root tenant from deactivation logic: skip when id equals MultitenancyConstants.Root.Id.
- Filter bulk scripts to non-root tenants before calling DeactivateAsync.
- If the root tenant should not appear as deactivatable, hide/disable the action in the UI for it.
- Correct the id if you intended to deactivate a different tenant.
Example fix
// before
foreach (var t in tenants)
await tenantService.DeactivateAsync(t.Id, ct);
// after
foreach (var t in tenants.Where(t => !string.Equals(t.Id, MultitenancyConstants.Root.Id, StringComparison.OrdinalIgnoreCase)))
await tenantService.DeactivateAsync(t.Id, ct); Defensive patterns
Strategy: validation
Validate before calling
if (string.Equals(id, MultitenancyConstants.Root.Id, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("The root tenant cannot be deactivated."); Type guard
bool IsRootTenant(AppTenantInfo t) => t.Id.Equals(MultitenancyConstants.Root.Id, StringComparison.OrdinalIgnoreCase);
Try / catch
try
{
await tenantService.DeactivateAsync(id, ct);
}
catch (CustomException ex) when (ex.Message.Contains("root tenant cannot be deactivated"))
{
logger.LogWarning("Attempted to deactivate root tenant {TenantId}", id);
} Prevention
- Always exclude MultitenancyConstants.Root.Id from bulk tenant operations.
- Hide the deactivate action for the root tenant in the admin UI.
- Compare tenant ids case-insensitively, matching the service's OrdinalIgnoreCase check.
- Never hardcode tenant id lists in scripts; derive them and filter root.
When it happens
Trigger: Calling DeactivateAsync with the root tenant id (constant Root.Id, typically the host/default tenant); scripts iterating all tenants and deactivating each without excluding root; passing an id differing only in case from the root id.
Common situations: Bulk offboarding jobs that don't filter out the root tenant; admins mistaking the root tenant for a normal tenant in the admin UI; scripts that hardcode ids and accidentally include root.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Storage quota exceeded
- Subscription cannot be backdated.
- Invalid Tenant
- ConnectionString can't be null.
- Cross-tenant audit summary requires…
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/a58f101b77dfccf5.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantService.cs:137
}
public async Task<string> DeactivateAsync(string id, CancellationToken cancellationToken = default)
{
var tenant = await GetTenantInfoAsync(id, cancellationToken).ConfigureAwait(false);
if (!tenant.IsActive)
{
throw new CustomException($"tenant {id} is already deactivated");
}
int tenantCount = (await _tenantStore.GetAllAsync().ConfigureAwait(false)).Count(t => t.IsActive);
if (tenantCount <= 1)
{
throw new CustomException("At least one active tenant is required.");
}
if (tenant.Id.Equals(MultitenancyConstants.Root.Id, StringComparison.OrdinalIgnoreCase))
{
throw new CustomException("The root tenant cannot be deactivated.");
}
tenant.Deactivate();
await _tenantStore.UpdateAsync(tenant).ConfigureAwait(false);
await RefreshTenantCacheAsync(tenant).ConfigureAwait(false);
return $"tenant {id} is now deactivated";
}
public async Task<bool> ExistsWithIdAsync(string id, CancellationToken cancellationToken = default) =>
await _tenantStore.GetAsync(id).ConfigureAwait(false) is not null;
public async Task<bool> ExistsWithNameAsync(string name, CancellationToken cancellationToken = default) =>
(await _tenantStore.GetAllAsync().ConfigureAwait(false)).Any(t => t.Name == name);
public async Task<PagedResponse<TenantDto>> GetAllAsync(GetTenantsQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
View on GitHub (pinned to 3f2959e683)