{"record":{"id":"4335a71febfb52cb","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required","errorCode":null,"errorMessage":"Tenant context is required.","messagePattern":"Tenant context is required\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GenerateInvoices/GenerateInvoicesCommandHandler.cs","lineNumber":22,"sourceCode":"using FSH.Modules.Billing.Contracts.v1.Invoices;\nusing FSH.Modules.Billing.Services;\nusing Mediator;\n\nnamespace FSH.Modules.Billing.Features.v1.Invoices.GenerateInvoices;\n\npublic sealed class GenerateInvoicesCommandHandler(\n    IBillingService billing,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : ICommandHandler<GenerateInvoicesCommand, int>\n{\n    public async ValueTask<int> Handle(GenerateInvoicesCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        // Platform-wide invoice generation runs across EVERY tenant — it is a root-operator action.\n        // A tenant admin (who also holds Billing.Manage) must not be able to trigger it.\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        if (callerTenantId != MultitenancyConstants.Root.Id)\n        {\n            throw new ForbiddenException(\"Only the root operator may generate invoices across tenants.\");\n        }\n\n        return await billing.GenerateInvoicesForAllTenantsAsync(command.PeriodYear, command.PeriodMonth, cancellationToken).ConfigureAwait(false);\n    }\n}\n","sourceCodeStart":4,"sourceCodeEnd":31,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GenerateInvoices/GenerateInvoicesCommandHandler.cs#L4-L31","documentation":"GenerateInvoicesCommandHandler throws UnauthorizedException when the caller has no tenant context (MultiTenantContext/TenantInfo is null). Platform-wide invoice generation is a root-operator action, so an anonymous or non-tenant-resolved caller is rejected first.","triggerScenarios":"Calling the generate-invoices endpoint without a valid tenant header/host so Finbuckle cannot resolve TenantInfo; calling from a background job or CLI without tenant context configured.","commonSituations":"Missing X-Tenant header in API calls; misconfigured tenant resolver for custom domains; invoking the endpoint from a Hangfire job or service client that doesn't set tenant context.","solutions":["Send the tenant identifier (X-Tenant header or tenant host) with the request.","Verify Finbuckle tenant resolution strategy and configured tenant mappings.","For automation, use the root tenant context explicitly (MultitenancyConstants.Root.Id).","If the endpoint should be non-tenant, move it to a root-scoped route with proper resolver config."],"exampleFix":"// before\nawait http.PostAsync(\"/api/v1/invoices/generate\", content); // no tenant header\n\n// after\nrequest.Headers.Add(\"X-Tenant\", \"root\");\nawait http.SendAsync(request);","handlingStrategy":"validation","validationCode":"var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id; if (string.IsNullOrEmpty(tenantId)) throw new UnauthorizedException(\"Tenant context is required.\");","typeGuard":null,"tryCatchPattern":"try { await api.GenerateInvoices(cmd); } catch (UnauthorizedAccessException) { // attach tenant header and retry once }","preventionTips":["Always send the tenant header on API clients","Configure tenant resolution for custom domains","Give background jobs an explicit tenant (root) context"],"tags":["multitenancy","authorization","billing","tenant-context"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}