fullstackhero/dotnet-starter-kit · error · CustomException

Tenant must retain at least one administrator.

Error message

Tenant must retain at least one administrator.

What it means

Thrown by EnsureMinimumAdminCountAsync after the pending removal is accounted for: if the tenant would end up with zero or one administrator, the change is rejected. Enforces the 'at least one active administrator' tenant invariant at the role level.

Solutions

  1. Promote another user to Admin before demoting the current one.
  2. Perform changes in an order that never drops admin count to 1.
  3. If the count looks wrong, verify RoleConstants.Admin matches the actual role name in the database.
  4. For tenant offboarding, transfer ownership first via a different admin account.

Example fix

// before: demote directly, may hit the minimum-admin guard
await userRoleService.AssignRolesAsync(soleAdminId, noAdminRoles, ct);
// after: promote a successor first
await userRoleService.AssignRolesAsync(successorId, rolesWithAdmin, ct);
await userRoleService.AssignRolesAsync(soleAdminId, noAdminRoles, ct);
Defensive patterns

Strategy: validation

Validate before calling

const admins = await apiFetch("/api/v1/users?role=Admin").then(r => r.length);
if (demotions >= admins - 1) throw new Error("Tenant must keep at least one administrator");

Try / catch

try { await assignRoles(userId, roles); } catch (e) {
  if (e.status === 400 && /at least one administrator/i.test(e.message)) notify("Promote a successor admin first");
  else throw e;
}

Prevention

When it happens

Trigger: Demoting or removing the Admin role from a user when the tenant currently has exactly one (or the last two) admins.

Common situations: Offboarding the only admin in a small tenant; bulk demotions that would exhaust admins; misconfigured role names so GetUsersInRoleAsync(Admin) undercounts (e.g. custom role constant changed).

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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserRoleService.cs:118

        }

        // After this removal, at least one admin must remain in the tenant — matches
        // the "at least one active administrator" invariant enforced on user deactivation.
        await EnsureMinimumAdminCountAsync();
    }

    private bool IsRootTenantAdmin(FshUser user)
    {
        return user.Email == MultitenancyConstants.Root.EmailAddress
            && multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id == MultitenancyConstants.Root.Id;
    }

    private async Task EnsureMinimumAdminCountAsync()
    {
        int adminCount = (await userManager.GetUsersInRoleAsync(RoleConstants.Admin)).Count;
        if (adminCount <= 1)
        {
            throw new CustomException(
                "Tenant must retain at least one administrator.",
                Array.Empty<string>(),
                HttpStatusCode.BadRequest);
        }
    }

    private async Task<List<string>> ProcessRoleAssignmentsAsync(FshUser user, List<UserRoleDto> userRoles)
    {
        var assignedRoles = new List<string>();

        foreach (var userRole in userRoles)
        {
            if (await roleManager.FindByNameAsync(userRole.RoleName!) is null)
            {
                continue;
            }

            if (userRole.Enabled)

View on GitHub (pinned to 3f2959e683)