fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

GetMyWalletQueryHandler resolves the caller's tenant id to get-or-create the wallet scoped to that tenant, because the billing store is not tenant-filtered and wallets are keyed by TenantId. With tenantAccessor.MultiTenantContext?.TenantInfo?.Id null, the handler cannot determine whose wallet to return and throws UnauthorizedException('Tenant context is required.').

Solutions

  1. Send the tenant identifier with the request (__tenant__ header or mapped tenant host).
  2. Register/verify the tenant in Finbuckle's tenant store so resolution succeeds.
  3. Ensure multitenancy middleware runs before the endpoint pipeline.
  4. In tests, fake ITenantAccessor to return TenantInfo with a fixed Id.

Example fix

// before
var res = await client.GetAsync("/api/v1/wallets/my");
// after
client.DefaultRequestHeaders.Add("__tenant__", "acme");
var res = await client.GetAsync("/api/v1/wallets/my");
Defensive patterns

Strategy: try-catch

Validate before calling

if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)
    throw new InvalidOperationException("Cannot resolve wallet owner: no tenant context.");

Type guard

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

Try / catch

try { var wallet = await api.GetMyWalletAsync(); }
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
    // set __tenant__ header / enter tenant scope, then retry once
}

Prevention

When it happens

Trigger: Requesting my-wallet without a resolvable tenant (no __tenant__ header, unmapped hostname), or calling the handler from a background job/CLI without a tenant context.

Common situations: New tenant not present in the tenant store so Finbuckle returns null TenantInfo; dashboard client deployed without runtime config supplying the tenant; service-to-service calls that dropped the tenant header.

Related errors


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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetMyWallet/GetMyWalletQueryHandler.cs:23

using FSH.Modules.Billing.Contracts.v1.Wallets;
using FSH.Modules.Billing.Mappings;
using FSH.Modules.Billing.Services;
using Mediator;

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

public sealed class GetMyWalletQueryHandler(
    IBillingService billingService,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : IQueryHandler<GetMyWalletQuery, WalletDto>
{
    public async ValueTask<WalletDto> Handle(GetMyWalletQuery query, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);

        // 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 wallet = await billingService.GetOrCreateWalletAsync(tenantId, "USD", cancellationToken).ConfigureAwait(false);
        return wallet.ToDto();
    }
}

View on GitHub (pinned to 3f2959e683)