fullstackhero/dotnet-starter-kit · warning · CustomException

tenant is already deactivated

Error message

tenant {id} is already deactivated

What it means

TenantService.DeactivateAsync throws a CustomException when the tenant is already inactive (IsActive == false), because deactivating an inactive tenant is a redundant state change. The guard is the first check in the method, before the active-tenant-count and root-tenant protections.

Solutions

  1. Check tenant.IsActive before calling DeactivateAsync and skip when already inactive.
  2. Catch CustomException and treat as success in idempotent batch jobs.
  3. If a later step in your pipeline failed after deactivation, resume from the failed step rather than re-deactivating.
  4. Use return message 'tenant {id} is now deactivated' from the first call as the signal it already happened.

Example fix

// before
tenants.ForEach(id => tenantService.DeactivateAsync(id, ct).Wait());

// after
foreach (var id in tenants)
{
    var t = await tenantService.GetAsync(id, ct);
    if (t.IsActive) await tenantService.DeactivateAsync(id, ct);
}
Defensive patterns

Strategy: validation

Validate before calling

var tenant = await tenantService.GetAsync(id, ct);
if (!tenant.IsActive) continue; // already inactive

Type guard

bool NeedsDeactivation(AppTenantInfo t) => t.IsActive;

Try / catch

try
{
    await tenantService.DeactivateAsync(id, ct);
}
catch (CustomException ex) when (ex.Message.Contains("already deactivated"))
{
    // idempotent success
}

Prevention

When it happens

Trigger: Calling DeactivateAsync on a tenant with IsActive == false; retrying a deactivation that already succeeded; two concurrent deactivation requests where the second arrives after the first persisted.

Common situations: Double-clicking 'Deactivate'; batch scripts deactivating a list that contains already-inactive tenants; re-running a failed pipeline whose deactivation step succeeded but a later step failed.

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/b6be8b961fcd8ec9. Report an issue: GitHub.

Appendix: source

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

    public async Task SeedTenantAsync(AppTenantInfo tenant, CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider.CreateScope();

        scope.ServiceProvider.GetRequiredService<IMultiTenantContextSetter>()
            .MultiTenantContext = new MultiTenantContext<AppTenantInfo>(tenant);

        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";
    }

View on GitHub (pinned to 3f2959e683)