fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

CreateTopupRequestCommandHandler resolves the caller's own tenant id because BillingDbContext is not tenant-filtered and the TopupRequest must be created with an explicit TenantId. When tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null (no tenant resolved by Finbuckle), it throws UnauthorizedException('Tenant context is required.') instead of writing an orphan (tenant-less) request.

Solutions

  1. Send the request with the tenant identifier: header __tenant__: <tenant-id-or-slug> or the mapped tenant host.
  2. Verify the tenant exists in the tenant store/connection-string mappings used by Finbuckle's strategy.
  3. Ensure multitenancy middleware is registered before endpoints in the host.
  4. In tests, stub ITenantAccessor to return TenantInfo with a non-null Id.

Example fix

// before
await client.PostAsJsonAsync("/api/v1/wallets/topup-requests", new { amount = 100 });
// after
client.DefaultRequestHeaders.Add("__tenant__", "acme");
await client.PostAsJsonAsync("/api/v1/wallets/topup-requests", new { amount = 100 });
Defensive patterns

Strategy: validation

Validate before calling

var tenant = request.Headers.TryGetValues("__tenant__", out var v) ? v.FirstOrDefault() : tenantFromHost;
if (string.IsNullOrWhiteSpace(tenant)) return Results.Unauthorized();

Type guard

bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;

Try / catch

try { await api.CreateTopupRequestAsync(amount, note); }
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
    // prompt user/tenant selection and retry with __tenant__ header
}

Prevention

When it happens

Trigger: Posting a create-top-up-request command without a resolvable tenant identifier (no __tenant__ header, unmapped hostname), or invoking the handler from a console/job context with no tenant context configured.

Common situations: curl/Postman tests missing the tenant header; a new tenant not yet registered in the tenant store so Finbuckle can't resolve it; CI integration tests omitting tenant setup; proxies stripping the host header used for tenant resolution.

Related errors


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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/CreateTopupRequest/CreateTopupRequestCommandHandler.cs:24

using FSH.Modules.Billing.Data;
using FSH.Modules.Billing.Domain;
using Mediator;

namespace FSH.Modules.Billing.Features.v1.Wallets.CreateTopupRequest;

public sealed class CreateTopupRequestCommandHandler(
    BillingDbContext db,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor,
    ICurrentUser currentUser)
    : ICommandHandler<CreateTopupRequestCommand, Guid>
{
    public async ValueTask<Guid> Handle(CreateTopupRequestCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        // BillingDbContext is not tenant-filtered; resolve caller's own tenant and scope strictly to it.
        var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");

        var requestedBy = currentUser.IsAuthenticated() ? currentUser.GetUserId().ToString() : null;
        var request = TopupRequest.Create(tenantId, command.Amount, "USD", command.Note, requestedBy);
        db.TopupRequests.Add(request);
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return request.Id;
    }
}

View on GitHub (pinned to 3f2959e683)