fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
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.').
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.
Example fix
// before
var res = await client.GetAsync("/api/v1/wallets/topup-requests?pageNumber=1");
// after
client.DefaultRequestHeaders.Add("__tenant__", "acme");
var res = await client.GetAsync("/api/v1/wallets/topup-requests?pageNumber=1"); Defensive patterns
Strategy: validation
Validate before calling
if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)
return Results.Unauthorized(); // derive no filter from a null tenant Type guard
bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;
Try / catch
try { var page = await api.GetTopupRequestsAsync(page, tenantId); }
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
// resend with __tenant__ header or use root context for cross-tenant view
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Tenant context is required.
- Tenant context is required.
- You can only approve top-up requests for your own tenant.
- Tenant context is required.
- Tenant context is required.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/042eff2669e09917.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetTopupRequests/GetTopupRequestsQueryHandler.cs:26
using FSH.Modules.Billing.Mappings;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Wallets.GetTopupRequests;
public sealed class GetTopupRequestsQueryHandler(
BillingDbContext dbContext,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: IQueryHandler<GetTopupRequestsQuery, PagedResponse<TopupRequestDto>>
{
public async ValueTask<PagedResponse<TopupRequestDto>> Handle(GetTopupRequestsQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
// BillingDbContext is not tenant-filtered: only root gets the cross-tenant view (optionally
// narrowed via query.TenantId); every other caller is forced to its own tenant.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var tenantFilter = isRoot ? query.TenantId : callerTenantId;
var q = dbContext.TopupRequests.AsNoTracking().AsQueryable();
if (!string.IsNullOrWhiteSpace(tenantFilter))
{
q = q.Where(r => r.TenantId == tenantFilter);
}
if (query.Status is not null)
{
q = q.Where(r => r.Status == query.Status);
}
var total = await q.LongCountAsync(cancellationToken).ConfigureAwait(false);
var items = await q
.OrderByDescending(r => r.CreatedAtUtc)
.Skip((query.PageNumber - 1) * query.PageSize)
.Take(query.PageSize)View on GitHub (pinned to 3f2959e683)