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

  1. Include a tenant identifier in the request (__tenant__ header, tenant subdomain, or route value per configured strategy).
  2. Verify Finbuckle multitenancy middleware registration and ordering in the host.
  3. For cross-tenant listing, use root credentials with root tenant context resolved, optionally passing query.TenantId.
  4. 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

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


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)