{"record":{"id":"8ff365d51df22362","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-getmyinvoicesqueryhandler","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/GetMyInvoices/GetMyInvoicesQueryHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Billing.Contracts.Dtos;\nusing FSH.Modules.Billing.Contracts.v1.Invoices;\nusing FSH.Modules.Billing.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Invoices.GetMyInvoices;\n\npublic sealed class GetMyInvoicesQueryHandler(\n    BillingDbContext dbContext,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : IQueryHandler<GetMyInvoicesQuery, PagedResponse<InvoiceDto>>\n{\n    public async ValueTask<PagedResponse<InvoiceDto>> Handle(GetMyInvoicesQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n        var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n\n        var q = dbContext.Invoices.AsNoTracking()\n            .Include(i => i.LineItems)\n            .Where(i => i.TenantId == tenantId);\n        if (query.Status is not null)\n        {\n            q = q.Where(i => i.Status == query.Status);\n        }\n        if (query.PeriodYear is not null)\n        {\n            q = q.Where(i => i.PeriodYear == query.PeriodYear);\n        }\n        if (query.PeriodMonth is not null)\n        {\n            q = q.Where(i => i.PeriodMonth == query.PeriodMonth);\n        }\n\n        var total = await q.LongCountAsync(cancellationToken).ConfigureAwait(false);","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetMyInvoices/GetMyInvoicesQueryHandler.cs#L5-L41","documentation":"GetMyInvoicesQueryHandler always scopes to the caller's own tenant: it reads tenantAccessor.MultiTenantContext.TenantInfo.Id and throws UnauthorizedException(\"Tenant context is required.\") if no tenant is resolved. There is no root bypass here by design — this endpoint is 'my invoices' only.","triggerScenarios":"GET /invoices/my (v1) invoked without tenant resolution: missing tenant token/header, unregistered host, request outside Finbuckle middleware, or a call from non-HTTP code with no tenant scope.","commonSituations":"A root/admin dashboard reusing the tenant-facing 'my invoices' endpoint without a tenant binding; SignalR/SSE or scheduled export jobs calling the handler directly; test harnesses constructing the query handler without tenant setup.","solutions":["Resolve a tenant for the caller (tenant header/query token or host mapping) before calling the endpoint.","If the intent is a cross-tenant listing, switch to GET /invoices with a root token and optional query.TenantId.","For background execution, run inside an explicit tenant scope.","Verify tenant middleware ordering so TenantInfo is populated before the query handler runs."],"exampleFix":"// before: root calling 'my' endpoint\nGET /invoices/my\n\n// after: root cross-tenant listing\nGET /invoices?tenantId=acme","handlingStrategy":"validation","validationCode":"if (!tenantId) throw new Error('GetMyInvoices requires a tenant context; resolve one first');","typeGuard":"function hasTenant(t) { return typeof t === 'string' && t.length > 0; }","tryCatchPattern":"try { return await apiFetch('/invoices/my'); }\ncatch (e) { if (isUnauthorized(e)) { redirectToTenantSelection(); return []; } throw e; }","preventionTips":["Use /invoices/my only from tenant-scoped clients; admins should use GET /invoices instead.","Resolve tenant context at app bootstrap and fail fast if absent.","Avoid calling this handler directly from jobs/tests without a tenant scope."],"tags":["multitenancy","authorization","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"}