{"record":{"id":"e4df12a983f25e8d","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-createtopuprequestcommandhandler","errorCode":null,"errorMessage":"Tenant context is required.","messagePattern":"Tenant context is required\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":403,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Wallets/CreateTopupRequest/CreateTopupRequestCommandHandler.cs","lineNumber":24,"sourceCode":"using FSH.Modules.Billing.Data;\nusing FSH.Modules.Billing.Domain;\nusing Mediator;\n\nnamespace FSH.Modules.Billing.Features.v1.Wallets.CreateTopupRequest;\n\npublic sealed class CreateTopupRequestCommandHandler(\n    BillingDbContext db,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor,\n    ICurrentUser currentUser)\n    : ICommandHandler<CreateTopupRequestCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(CreateTopupRequestCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        // BillingDbContext is not tenant-filtered; resolve caller's own tenant and scope strictly to it.\n        var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n\n        var requestedBy = currentUser.IsAuthenticated() ? currentUser.GetUserId().ToString() : null;\n        var request = TopupRequest.Create(tenantId, command.Amount, \"USD\", command.Note, requestedBy);\n        db.TopupRequests.Add(request);\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return request.Id;\n    }\n}\n","sourceCodeStart":6,"sourceCodeEnd":33,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/CreateTopupRequest/CreateTopupRequestCommandHandler.cs#L6-L33","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the request with the tenant identifier: header __tenant__: <tenant-id-or-slug> or the mapped tenant host.","Verify the tenant exists in the tenant store/connection-string mappings used by Finbuckle's strategy.","Ensure multitenancy middleware is registered before endpoints in the host.","In tests, stub ITenantAccessor to return TenantInfo with a non-null Id."],"exampleFix":"// before\nawait client.PostAsJsonAsync(\"/api/v1/wallets/topup-requests\", new { amount = 100 });\n// after\nclient.DefaultRequestHeaders.Add(\"__tenant__\", \"acme\");\nawait client.PostAsJsonAsync(\"/api/v1/wallets/topup-requests\", new { amount = 100 });","handlingStrategy":"validation","validationCode":"var tenant = request.Headers.TryGetValues(\"__tenant__\", out var v) ? v.FirstOrDefault() : tenantFromHost;\nif (string.IsNullOrWhiteSpace(tenant)) return Results.Unauthorized();","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try { await api.CreateTopupRequestAsync(amount, note); }\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    // prompt user/tenant selection and retry with __tenant__ header\n}","preventionTips":["Ensure the tenant is registered in the tenant store before its users call wallet endpoints.","Inject __tenant__ in one place (client interceptor) rather than per-call.","Add integration tests that create top-up requests per tenant.","Check middleware order after upgrading Finbuckle or the host pipeline."],"tags":["multitenancy","authorization","wallet","billing"],"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"}