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

  1. Exclude the root tenant from deactivation logic: skip when id equals MultitenancyConstants.Root.Id.
  2. Filter bulk scripts to non-root tenants before calling DeactivateAsync.
  3. If the root tenant should not appear as deactivatable, hide/disable the action in the UI for it.
  4. 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

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


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)