{"record":{"id":"042eff2669e09917","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-gettopuprequestsqueryhandler","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/GetTopupRequests/GetTopupRequestsQueryHandler.cs","lineNumber":26,"sourceCode":"using FSH.Modules.Billing.Mappings;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Wallets.GetTopupRequests;\n\npublic sealed class GetTopupRequestsQueryHandler(\n    BillingDbContext dbContext,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : IQueryHandler<GetTopupRequestsQuery, PagedResponse<TopupRequestDto>>\n{\n    public async ValueTask<PagedResponse<TopupRequestDto>> Handle(GetTopupRequestsQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n        // BillingDbContext is not tenant-filtered: only root gets the cross-tenant view (optionally\n        // narrowed via query.TenantId); every other caller is forced to its own tenant.\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n        var tenantFilter = isRoot ? query.TenantId : callerTenantId;\n\n        var q = dbContext.TopupRequests.AsNoTracking().AsQueryable();\n        if (!string.IsNullOrWhiteSpace(tenantFilter))\n        {\n            q = q.Where(r => r.TenantId == tenantFilter);\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)","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetTopupRequests/GetTopupRequestsQueryHandler.cs#L8-L44","documentation":"GetTopupRequestsQueryHandler derives its tenant filter from the caller: root gets a cross-tenant view (optionally narrowed by query.TenantId), all other callers are forced to their own tenant. Since TopupRequests is not tenant-filtered by the DbContext, a null caller tenant id leaves no safe filter, so the handler throws UnauthorizedException('Tenant context is required.').","triggerScenarios":"Listing top-up requests over HTTP with no Finbuckle-resolvable tenant identifier, or invoking the query from a Hangfire/CLI context where no tenant context was entered.","commonSituations":"Admin listing tool calling with root token but no root tenant context resolved; reverse proxy stripping the tenant host header; automated tests with default (null) tenant accessors.","solutions":["Include a tenant identifier in the request (__tenant__ header, tenant subdomain, or route value per configured strategy).","Verify Finbuckle multitenancy middleware registration and ordering in the host.","For cross-tenant listing, use root credentials with root tenant context resolved, optionally passing query.TenantId.","In tests, stub ITenantAccessor so MultiTenantContext.TenantInfo.Id is non-null."],"exampleFix":"// before\nvar res = await client.GetAsync(\"/api/v1/wallets/topup-requests?pageNumber=1\");\n// after\nclient.DefaultRequestHeaders.Add(\"__tenant__\", \"acme\");\nvar res = await client.GetAsync(\"/api/v1/wallets/topup-requests?pageNumber=1\");","handlingStrategy":"validation","validationCode":"if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)\n    return Results.Unauthorized(); // derive no filter from a null tenant","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try { var page = await api.GetTopupRequestsAsync(page, tenantId); }\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    // resend with __tenant__ header or use root context for cross-tenant view\n}","preventionTips":["Document that root must still present root tenant context to list across tenants.","Centralize tenant propagation in HTTP client handlers and job wrappers.","Add regression tests for endpoints that manually derive tenant filters.","Verify proxy configurations preserve tenant resolution headers/hosts."],"tags":["multitenancy","authorization","wallet","query"],"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"}