{"record":{"id":"56c4b78055d2f9c6","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-context-is-required-getsubscriptionqueryhandler","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/Subscriptions/GetSubscription/GetSubscriptionQueryHandler.cs","lineNumber":22,"sourceCode":"using FSH.Modules.Billing.Contracts.Dtos;\nusing FSH.Modules.Billing.Contracts.v1.Subscriptions;\nusing FSH.Modules.Billing.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Subscriptions.GetSubscription;\n\npublic sealed class GetSubscriptionQueryHandler(\n    BillingDbContext dbContext,\n    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : IQueryHandler<GetSubscriptionQuery, SubscriptionDto?>\n{\n    public async ValueTask<SubscriptionDto?> Handle(GetSubscriptionQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n\n        // BillingDbContext is not tenant-filtered, so a tenant caller is pinned to its OWN\n        // subscription and only root may pass an arbitrary tenant id (else cross-tenant reads).\n        var tenantId = callerTenantId == MultitenancyConstants.Root.Id\n            ? query.TenantId ?? callerTenantId\n            : callerTenantId;\n\n        var sub = await (from s in dbContext.Subscriptions.AsNoTracking()\n                         join p in dbContext.Plans.AsNoTracking() on s.PlanId equals p.Id\n                         where s.TenantId == tenantId\n                            && s.Status == Contracts.SubscriptionStatus.Active\n                         select new SubscriptionDto(s.Id, s.TenantId, s.PlanId, p.Key, s.StartUtc, s.EndUtc, s.Status))\n                        .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);\n        return sub;\n    }\n}\n","sourceCodeStart":4,"sourceCodeEnd":39,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/GetSubscription/GetSubscriptionQueryHandler.cs#L4-L39","documentation":"This handler throws UnauthorizedException('Tenant context is required.') when tenantAccessor.MultiTenantContext?.TenantInfo?.Id resolves to null. BillingDbContext is not tenant-filtered, so the handler must resolve the caller's tenant id explicitly to pin reads to that tenant; if Finbuckle has not resolved a tenant for the request, no safe tenant scope exists and the handler refuses to run. It is an application-level guard, not an infrastructure failure.","triggerScenarios":"Calling GET subscription endpoint without a tenant identifier resolvable by Finbuckle — e.g. request missing the __tenant__ header/route/query value, hostname not mapped to a tenant in TenantStore, or the call made from a background/Hangfire/CLI context with no TenantContext set.","commonSituations":"Testing the endpoint with curl/Postman omitting the tenant header; a reverse proxy stripping the tenant host header; calling the API as root/admin without impersonating a tenant; integration tests that build the handler manually without seeding ITenantInfo; middleware ordering where multitenancy middleware runs after the endpoint pipeline.","solutions":["Pass a tenant identifier with the request: add header __tenant__: <tenant-id-or-identifier> (or use the tenant-resolving hostname/route value configured in the app).","Verify Finbuckle multitenancy middleware is registered and ordered before endpoint mapping in the host pipeline.","If calling from a non-HTTP context (job/CLI), set the tenant explicitly (e.g. ITenantContext/TenantInfo scoped service or tenant context accessor) before invoking the handler.","In tests, register/configure a mock ITenantAccessor whose MultiTenantContext.TenantInfo.Id returns a non-null tenant id."],"exampleFix":"// before\ncurl -H \"Authorization: Bearer $TOKEN\" https://localhost:7030/api/v1/subscriptions/3fa85f64...\n// after\ncurl -H \"Authorization: Bearer $TOKEN\" -H \"__tenant__: acme\" https://localhost:7030/api/v1/subscriptions/3fa85f64...","handlingStrategy":"try-catch","validationCode":"var tenantId = request.Headers.TryGetValues(\"__tenant__\", out var v) ? v.FirstOrDefault() : null;\nif (string.IsNullOrWhiteSpace(tenantId)) throw new InvalidOperationException(\"__tenant__ header required before calling GetSubscription\");","typeGuard":"bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;","tryCatchPattern":"try\n{\n    var sub = await api.GetSubscriptionAsync(id);\n}\ncatch (UnauthorizedException ex) when (ex.Message == \"Tenant context is required.\")\n{\n    logger.LogWarning(ex, \"No tenant context resolved; re-issue with __tenant__ header\");\n    // re-dispatch with tenant header or surface a 401 to the caller\n}","preventionTips":["Always attach the __tenant__ header (or use the tenant's mapped host) in API clients and test fixtures.","Add an integration-test asserting every Billing endpoint returns 401 (not 500) when tenant context is missing.","Never call Billing handlers from background jobs without entering a tenant scope first.","Keep multitenancy middleware registered before UseEndpoints/MapEndpoints."],"tags":["multitenancy","authorization","billing","finbuckle"],"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"}