fullstackhero/dotnet-starter-kit · warning · CustomException

tenant is already activated

Error message

tenant {id} is already activated

What it means

TenantService.ActivateAsync throws a CustomException when the tenant's IsActive flag is already true, since activating an active tenant is a no-op conflict. The check runs before provisioning validation (EnsureCanActivateAsync) and before tenant.Activate() persists the state change.

Solutions

  1. Check tenant.IsActive before calling ActivateAsync and skip when already active.
  2. Treat this error as success in idempotent automation (catch CustomException and continue).
  3. If activation previously failed after flipping IsActive, fix the downstream step instead of re-activating.
  4. Serialize activation calls per tenant to avoid concurrent duplicate requests.

Example fix

// before
await tenantService.ActivateAsync(tenantId, ct);

// after
var tenant = await tenantService.GetAsync(tenantId, ct);
if (!tenant.IsActive)
{
    await tenantService.ActivateAsync(tenantId, ct);
}
Defensive patterns

Strategy: validation

Validate before calling

var tenant = await tenantService.GetAsync(id, ct);
if (tenant.IsActive) return; // nothing to do

Type guard

bool NeedsActivation(AppTenantInfo t) => !t.IsActive;

Try / catch

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

Prevention

When it happens

Trigger: Calling ActivateAsync on a tenant whose IsActive is true; retrying an activation that already succeeded; concurrent activation requests from two clients; activating a tenant that was created already-active.

Common situations: Double-clicking 'Activate'; an automation pipeline that activates unconditionally on each run; an earlier activation partially succeeded (tenant flipped active but later steps failed), so re-running hits this guard.

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

Appendix: source

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

        ArgumentNullException.ThrowIfNull(config);
        ArgumentNullException.ThrowIfNull(billingOptions);
        _tenantStore = tenantStore;
        _config = config.Value;
        _serviceProvider = serviceProvider;
        _dbContext = dbContext;
        _provisioningService = provisioningService;
        _timeProvider = timeProvider;
        _billingOptions = billingOptions.Value;
        _logger = logger;
    }

    public async Task<string> ActivateAsync(string id, CancellationToken cancellationToken)
    {
        var tenant = await GetTenantInfoAsync(id, cancellationToken).ConfigureAwait(false);

        if (tenant.IsActive)
        {
            throw new CustomException($"tenant {id} is already activated");
        }

        await _provisioningService.EnsureCanActivateAsync(id, cancellationToken).ConfigureAwait(false);

        tenant.Activate();

        await _tenantStore.UpdateAsync(tenant).ConfigureAwait(false);
        await RefreshTenantCacheAsync(tenant).ConfigureAwait(false);

        return $"tenant {id} is now activated";
    }

    public async Task<string> CreateAsync(string id,
        string name,
        string? connectionString,
        string adminEmail, string? issuer, string planKey, DateTime validUpto, CancellationToken cancellationToken)
    {
        if (connectionString?.Trim() == _config.ConnectionString.Trim())

View on GitHub (pinned to 3f2959e683)