fullstackhero/dotnet-starter-kit · error · NotFoundException

Theme for tenant not found

Error message

Theme for tenant {tenantId} not found

What it means

TenantThemeService.SetAsDefaultThemeAsync looks up the TenantTheme row for the given tenantId and throws NotFoundException when no theme record exists for that tenant. Themes must be created before one can be promoted to default; the service refuses to implicitly create one. The exception maps to an HTTP 404 for the caller.

Solutions

  1. Run the theme seeding/migration (dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed) so the tenant gets a TenantTheme row.
  2. Create a theme for the tenant first (CreateTheme endpoint/handler), then call SetAsDefaultThemeAsync.
  3. Verify the tenantId is correct and matches the tenant that actually owns the theme row (watch out for cross-environment tenant IDs).
  4. Check the TenantThemes table for the row: SELECT * FROM "TenantThemes" WHERE "TenantId" = '<tenantId>'.

Example fix

// before
await tenantThemeService.SetAsDefaultThemeAsync(tenantId, ct); // throws if no theme exists
// after
var theme = await tenantThemeService.GetThemeAsync(tenantId, ct);
if (theme is null)
{
    await tenantThemeService.CreateDefaultThemeAsync(tenantId, ct); // ensure a theme exists
}
await tenantThemeService.SetAsDefaultThemeAsync(tenantId, ct);
Defensive patterns

Strategy: try-catch

Validate before calling

var theme = await tenantThemeService.GetThemeAsync(tenantId, ct);
if (theme is null) throw new InvalidOperationException($"No theme exists for tenant {tenantId}; create one before setting default.");

Type guard

bool HasTheme(ThemeDto? theme) => theme is not null && !string.IsNullOrWhiteSpace(theme.TenantId);

Try / catch

try
{
    await tenantThemeService.SetAsDefaultThemeAsync(tenantId, ct);
}
catch (NotFoundException)
{
    logger.LogWarning("No theme found for tenant {TenantId}; creating one first.", tenantId);
    await tenantThemeService.CreateDefaultThemeAsync(tenantId, ct);
    await tenantThemeService.SetAsDefaultThemeAsync(tenantId, ct);
}

Prevention

When it happens

Trigger: Calling SetAsDefaultThemeAsync(tenantId) (or the SetDefaultTheme endpoint) when the tenant has no row in the TenantThemes table — i.e. no theme was ever created/seeded for that tenant, or the theme row was deleted.

Common situations: Running against a fresh database where the theme seeder did not run; calling the set-default endpoint before the tenant customized a theme; using a tenantId from another environment (staging vs prod) where the theme record is missing; a manual cleanup deleted the TenantTheme row.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

Thrown at src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantThemeService.cs:243

        // Clear existing default
        var existingDefault = await _dbContext.TenantThemes
            .FirstOrDefaultAsync(t => t.IsDefault, ct)
            .ConfigureAwait(false);

        if (existingDefault is not null)
        {
            existingDefault.IsDefault = false;
        }

        // Set new default
        var entity = await _dbContext.TenantThemes
            .FirstOrDefaultAsync(t => t.TenantId == tenantId, ct)
            .ConfigureAwait(false);

        if (entity is null)
        {
            throw new NotFoundException($"Theme for tenant {tenantId} not found");
        }

        entity.IsDefault = true;
        await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false);

        // Invalidate default theme cache
        await _cache.RemoveAsync(CacheKeys.DefaultTheme, ct).ConfigureAwait(false);

        if (_logger.IsEnabled(LogLevel.Information))
        {
            _logger.LogInformation("Set theme for tenant {TenantId} as default", tenantId);
        }
    }

    public async Task InvalidateCacheAsync(string tenantId, CancellationToken ct = default)
    {
        // Purge both the tenant-specific entry and anything tagged for this tenant.
        await _cache.RemoveAsync(CacheKeys.TenantTheme(tenantId), ct).ConfigureAwait(false);

View on GitHub (pinned to 3f2959e683)