fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

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.').

Solutions

  1. Add the tenant identifier to the request (__tenant__ header or the tenant's mapped hostname).
  2. Check the multitenancy strategy configuration matches how the client sends the tenant (header vs host vs path).
  3. Ensure Finbuckle middleware ordering is before UseEndpoints.
  4. In unit tests, provide a mock ITenantAccessor returning a TenantInfo with Id set.

Example fix

// before
var res = await client.GetAsync("/api/v1/wallets/my-topup-requests?pageNumber=1");
// after
client.DefaultRequestHeaders.Add("__tenant__", "acme");
var res = await client.GetAsync("/api/v1/wallets/my-topup-requests?pageNumber=1");
Defensive patterns

Strategy: try-catch

Validate before calling

if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id is null)
    throw new InvalidOperationException("GetMyTopupRequests needs a tenant context (__tenant__ header or tenant host).");

Type guard

bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;

Try / catch

try { var page = await api.GetMyTopupRequestsAsync(page); }
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
    // re-authenticate/resend with tenant identifier
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/6f8046165afad3e4. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/GetMyTopupRequests/GetMyTopupRequestsQueryHandler.cs:25

using FSH.Modules.Billing.Data;
using FSH.Modules.Billing.Mappings;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Billing.Features.v1.Wallets.GetMyTopupRequests;

public sealed class GetMyTopupRequestsQueryHandler(
    BillingDbContext dbContext,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : IQueryHandler<GetMyTopupRequestsQuery, PagedResponse<TopupRequestDto>>
{
    public async ValueTask<PagedResponse<TopupRequestDto>> Handle(GetMyTopupRequestsQuery query, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);

        // BillingDbContext is not tenant-filtered; resolve caller's own tenant and scope strictly to it.
        var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");

        var q = dbContext.TopupRequests.AsNoTracking()
            .Where(r => r.TenantId == tenantId);

        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)
            .ToListAsync(cancellationToken).ConfigureAwait(false);

        return new PagedResponse<TopupRequestDto>
        {

View on GitHub (pinned to 3f2959e683)