fullstackhero/dotnet-starter-kit · error · CustomException

At least one active tenant is required.

Error message

At least one active tenant is required.

What it means

DeactivateAsync counts active tenants across the store and refuses when the deactivation would leave zero active tenants (tenantCount <= 1). At least one active tenant must always exist so the system keeps a usable default/root tenancy. Note the count is computed from GetAllAsync, so all tenants are considered, not just the one being deactivated.

Solutions

  1. Activate another tenant first so at least two are active, then deactivate the target.
  2. Re-create/activate the root tenant if it was inadvertently deactivated elsewhere.
  3. In automation, count active tenants before deactivating and stop at one.
  4. If this is a seeded dev environment, reseed tenants so more than one is active.

Example fix

// before
await tenantService.DeactivateAsync(lastActiveTenantId, ct);

// after
var all = await tenantService.GetAllAsync(ct);
var activeCount = all.Count(t => t.IsActive);
if (activeCount <= 1)
    throw new InvalidOperationException("Activate another tenant before deactivating the last active one.");
await tenantService.DeactivateAsync(lastActiveTenantId, ct);
Defensive patterns

Strategy: validation

Validate before calling

var all = await tenantService.GetAllAsync(ct);
if (all.Count(t => t.IsActive) <= 1)
    throw new InvalidOperationException("Refusing: this would leave zero active tenants. Activate another first.");

Type guard

bool CanDeactivate(IEnumerable<AppTenantInfo> all, string id) =>
    all.Count(t => t.IsActive && !t.Id.Equals(id, StringComparison.OrdinalIgnoreCase)) >= 1;

Try / catch

try
{
    await tenantService.DeactivateAsync(id, ct);
}
catch (CustomException ex) when (ex.Message == "At least one active tenant is required.")
{
    logger.LogWarning("Deactivation of {TenantId} blocked: last active tenant", id);
}

Prevention

When it happens

Trigger: Deactivating the last remaining active tenant; deactivating the root tenant when it is the only active one (count check fires before the root check); environments where all other tenants were already deactivated or deleted.

Common situations: Clean-up scripts deactivating tenants one by one until only one remains; test/seed environments with a single tenant; tenant offboarding that didn't account for the always-one-active invariant.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/14ad4d4f8d04a359. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantService.cs:132

        foreach (var initializer in scope.ServiceProvider.GetServices<IDbInitializer>())
        {
            await initializer.SeedAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    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);

View on GitHub (pinned to 3f2959e683)