{"record":{"id":"2bbdd0434af30993","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-getmywalletqueryhandler","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/GetMyWallet/GetMyWalletQueryHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Billing.Contracts.v1.Wallets;\nusing FSH.Modules.Billing.Mappings;\nusing FSH.Modules.Billing.Services;\nusing Mediator;\n\nnamespace FSH.Modules.Billing.Features.v1.Wallets.GetMyWallet;\n\npublic sealed class GetMyWalletQueryHandler(\n    IBillingService billingService,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : IQueryHandler<GetMyWalletQuery, WalletDto>\n{\n    public async ValueTask<WalletDto> Handle(GetMyWalletQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\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 wallet = await billingService.GetOrCreateWalletAsync(tenantId, \"USD\", cancellationToken).ConfigureAwait(false);\n        return wallet.ToDto();\n    }\n}\n","sourceCodeStart":5,"sourceCodeEnd":29,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetMyWallet/GetMyWalletQueryHandler.cs#L5-L29","documentation":"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.').","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the tenant identifier with the request (__tenant__ header or mapped tenant host).","Register/verify the tenant in Finbuckle's tenant store so resolution succeeds.","Ensure multitenancy middleware runs before the endpoint pipeline.","In tests, fake ITenantAccessor to return TenantInfo with a fixed Id."],"exampleFix":"// before\nvar res = await client.GetAsync(\"/api/v1/wallets/my\");\n// after\nclient.DefaultRequestHeaders.Add(\"__tenant__\", \"acme\");\nvar res = await client.GetAsync(\"/api/v1/wallets/my\");","handlingStrategy":"try-catch","validationCode":"if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)\n    throw new InvalidOperationException(\"Cannot resolve wallet owner: no tenant context.\");","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try { var wallet = await api.GetMyWalletAsync(); }\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    // set __tenant__ header / enter tenant scope, then retry once\n}","preventionTips":["Provision tenants in the tenant store as part of onboarding before wallet calls.","Include tenant header in service-to-service calls (propagate from incoming request).","Test get-or-create wallet flows per tenant in CI.","Keep multitenancy middleware first in the 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"}