{"record":{"id":"6f8046165afad3e4","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-getmytopuprequestsqueryhandler","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/GetMyTopupRequests/GetMyTopupRequestsQueryHandler.cs","lineNumber":25,"sourceCode":"using FSH.Modules.Billing.Data;\nusing FSH.Modules.Billing.Mappings;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Wallets.GetMyTopupRequests;\n\npublic sealed class GetMyTopupRequestsQueryHandler(\n    BillingDbContext dbContext,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : IQueryHandler<GetMyTopupRequestsQuery, PagedResponse<TopupRequestDto>>\n{\n    public async ValueTask<PagedResponse<TopupRequestDto>> Handle(GetMyTopupRequestsQuery 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 q = dbContext.TopupRequests.AsNoTracking()\n            .Where(r => r.TenantId == tenantId);\n\n        if (query.Status is not null)\n        {\n            q = q.Where(r => r.Status == query.Status);\n        }\n\n        var total = await q.LongCountAsync(cancellationToken).ConfigureAwait(false);\n        var items = await q\n            .OrderByDescending(r => r.CreatedAtUtc)\n            .Skip((query.PageNumber - 1) * query.PageSize)\n            .Take(query.PageSize)\n            .ToListAsync(cancellationToken).ConfigureAwait(false);\n\n        return new PagedResponse<TopupRequestDto>\n        {","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetMyTopupRequests/GetMyTopupRequestsQueryHandler.cs#L7-L43","documentation":"GetMyTopupRequestsQueryHandler pins the query strictly to the caller's own tenant (TopupRequests is not tenant-filtered by the DbContext), so it needs tenantAccessor.MultiTenantContext?.TenantInfo?.Id. A null tenant id means 'my tenant' is undefined, and the handler throws UnauthorizedException('Tenant context is required.').","triggerScenarios":"Calling the my-top-up-requests endpoint with no Finbuckle-resolvable tenant (missing __tenant__ header, unmapped host), or invoking the query from a non-HTTP context without tenant context.","commonSituations":"Omitting the tenant header in Swagger/curl after switching APIs; host-based tenant strategy broken behind a reverse proxy that rewrites Host; integration tests that forgot tenant seeding.","solutions":["Add the tenant identifier to the request (__tenant__ header or the tenant's mapped hostname).","Check the multitenancy strategy configuration matches how the client sends the tenant (header vs host vs path).","Ensure Finbuckle middleware ordering is before UseEndpoints.","In unit tests, provide a mock ITenantAccessor returning a TenantInfo with Id set."],"exampleFix":"// before\nvar res = await client.GetAsync(\"/api/v1/wallets/my-topup-requests?pageNumber=1\");\n// after\nclient.DefaultRequestHeaders.Add(\"__tenant__\", \"acme\");\nvar res = await client.GetAsync(\"/api/v1/wallets/my-topup-requests?pageNumber=1\");","handlingStrategy":"try-catch","validationCode":"if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)\n    throw new InvalidOperationException(\"GetMyTopupRequests needs a tenant context (__tenant__ header or tenant host).\");","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try { var page = await api.GetMyTopupRequestsAsync(page); }\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    // re-authenticate/resend with tenant identifier\n}","preventionTips":["Keep tenant header injection in a shared API client wrapper.","Verify host-based tenant resolution behind proxies with an e2e test.","Seed tenant context in every integration test fixture.","Fail fast in middleware when TenantInfo is null for tenant-scoped routes."],"tags":["multitenancy","authorization","wallet","pagination"],"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"}