fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
CaptureUsageSnapshotsCommandHandler requires a resolved caller tenant before capturing usage snapshots, because the usage reporter must be scoped to a concrete tenant and the handler uses the caller tenant to decide whether arbitrary-tenant capture (root only) is allowed. When tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null it throws UnauthorizedException('Tenant context is required.'). This keeps usage/overage snapshots from being written without a tenant scope.
Solutions
- Send the request with a resolvable tenant id (header __tenant__: <id> or mapped tenant host).
- If capture should run for many tenants, iterate tenants and set the tenant context per iteration inside the job instead of calling without one.
- Register Finbuckle multitenancy middleware before UseEndpoints in the host, and confirm the strategy (header/host/path) matches how you send the tenant.
- In unit tests, stub ITenantAccessor to return a TenantInfo with a non-null Id.
Example fix
// before
await mediator.Send(new CaptureUsageSnapshotsCommand(2026, 9, tenantId: null));
// after (HTTP)
await client.PostAsJsonAsync("/api/v1/usage/snapshots/capture", body, ...); // with __tenant__ header set
// or in a job:
using (tenantScope.Enter(tenantId)) { await mediator.Send(cmd); } Defensive patterns
Strategy: validation
Validate before calling
if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)
throw new InvalidOperationException("CaptureUsageSnapshots requires a resolved tenant context (set __tenant__ or enter a tenant scope)."); Type guard
bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is { Length: > 0 }; Try / catch
try { await mediator.Send(new CaptureUsageSnapshotsCommand(year, month, tenantId)); }
catch (UnauthorizedException ex) when (ex.Message.Contains("Tenant context is required"))
{
logger.LogWarning("Snapshot capture skipped: no tenant context");
} Prevention
- Wrap recurring Hangfire capture jobs in a per-tenant scope and iterate tenants explicitly.
- Verify Finbuckle middleware ordering whenever adding a new host/edge proxy.
- Add a startup smoke test that calls the endpoint with a tenant header to catch resolution regressions.
- Stub ITenantAccessor in unit tests instead of relying on ambient context.
When it happens
Trigger: Invoking the capture-usage-snapshots command from a context where Finbuckle has no tenant resolution: missing __tenant__ header on the HTTP call, unmapped host, or a Hangfire job/CLI invocation that never set a tenant.
Common situations: Scheduling the snapshot capture as a recurring job without wrapping execution in a tenant scope; admin tools calling the endpoint without a tenant header; tests constructing the handler with a default ITenantAccessor returning null.
Related errors
- Tenant context is required.
- Cross-tenant audit summary requires…
- Cross-tenant audit access requires…
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/bf2a4f361e24a657.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Usage/CaptureUsageSnapshots/CaptureUsageSnapshotsCommandHandler.cs:24
using FSH.Modules.Billing.Services;
using Mediator;
namespace FSH.Modules.Billing.Features.v1.Usage.CaptureUsageSnapshots;
public sealed class CaptureUsageSnapshotsCommandHandler(
IUsageReporter reporter,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: ICommandHandler<CaptureUsageSnapshotsCommand, IReadOnlyList<UsageSnapshotDto>>
{
public async ValueTask<IReadOnlyList<UsageSnapshotDto>> Handle(
CaptureUsageSnapshotsCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
// Only the root operator may capture usage for an arbitrary tenant; a tenant caller is pinned
// to its own tenant so it can't fabricate another tenant's usage/overage snapshots.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var targetTenantId = isRoot ? command.TenantId : callerTenantId;
var snapshots = await reporter
.CaptureForPeriodAsync(targetTenantId, command.PeriodYear, command.PeriodMonth, cancellationToken)
.ConfigureAwait(false);
return snapshots
.Select(s => new UsageSnapshotDto(
s.Id,
s.TenantId,
s.PeriodYear,
s.PeriodMonth,
s.Resource,
s.UsedUnits,
s.LimitUnits,
s.Overage,
s.CapturedAtUtc))View on GitHub (pinned to 3f2959e683)