Troubleshooting
Practical problem/solution guide for Pragmatic.MultiTenancy. Each section covers a common issue, the likely causes, and the fix.
TenantId Is Always Null
Section titled “TenantId Is Always Null”The tenant is never resolved — ITenantContext.IsResolved is false and TenantId is null.
Checklist
Section titled “Checklist”-
Did you configure a resolution strategy? Without
app.UseMultiTenancy(mt => ...), the SG registersSingleTenantResolverwhich returns"default"(not null). If you see null, it means either no resolver is registered or the resolver is failing silently. -
Is the middleware in the pipeline? In
PragmaticApp.RunAsync, the middleware is added automatically. If you are manually composing the pipeline, ensureapp.UseMiddleware<TenantResolutionMiddleware>()is called. -
For header-based resolution: Is the client sending the
X-Tenant-Idheader? Check the header name inMultiTenancyOptions.TenantHeaderName. -
For claim-based resolution: Is authentication middleware running before tenant resolution? The
ClaimTenantResolverreadsHttpContext.User, which is only populated afterUseAuthentication(). Also verify the claim type matches (MultiTenancyOptions.TenantClaimType, default:"tenant_id"). -
For subdomain-based resolution: Does the host have at least 3 segments?
localhostandapp.comreturn null. Onlysubdomain.domain.tld(3+ segments) resolves a tenant. -
For route-based resolution: Is the route parameter name correct? Default is
"tenantId". The route template must include{tenantId}(or your custom parameter name). -
Check the logs. Enable
Debuglevel forPragmatic.MultiTenancy. The middleware logs"Tenant '{TenantId}' resolved for {RequestPath}"on success and"No tenant resolved for {RequestPath}"when null.
Queries Return Data From All Tenants
Section titled “Queries Return Data From All Tenants”Tenant filtering is not applied — queries return rows belonging to other tenants.
Checklist
Section titled “Checklist”-
Does the entity implement
ITenantEntity? The SG only generatesTenantFilterfor entities with theITenantEntityinterface. Having aTenantIdproperty without the interface is not enough. -
Is the SG running? Check that the SG reference is in the
.csprojwithOutputItemType="Analyzer". Look for the generated{Entity}.TenantFilter.g.csfile inobj/. -
Is
ITenantContextresolved correctly? IfITenantContext.TenantIdis null at the time the query executes, the filter might not match any rows (or might return all rows, depending on the null-comparison semantics of your database). -
Are you using raw SQL or
IQueryableoutside the repository?TenantFilteris applied by the Pragmatic query pipeline. If you bypass the pipeline with raw SQL or directDbContextaccess, the filter is not applied. Always use the repository abstraction. -
Is TenantScope set for non-HTTP contexts? In background jobs and tests, use
TenantScope.BeginScope("tenant-id")before executing queries.
Wrong Tenant Resolved
Section titled “Wrong Tenant Resolved”The tenant is resolved, but it is the wrong value.
Possible Causes
Section titled “Possible Causes”-
Header name mismatch. The client sends
X-Organization-Idbut the resolver readsX-Tenant-Id(default). Fix:mt.UseHeader("X-Organization-Id"). -
Claim type mismatch. The JWT has
org_idbut the resolver readstenant_id(default). Fix:mt.UseClaim("org_id"). -
Composite resolver order. With chained resolvers, the first non-null result wins. If header-based is first and the header has a stale value, it takes precedence over the JWT claim. Reorder the chain or remove the header strategy.
-
Subdomain parsing. The subdomain resolver takes the first segment. For
beta.staging.app.com, the tenant isbeta, notstaging. If your tenant is the second segment, implement a custom resolver.
TenantScope Not Flowing in Async Code
Section titled “TenantScope Not Flowing in Async Code”TenantScope.BeginScope is called but the tenant is not visible in async continuations.
Checklist
Section titled “Checklist”-
Is the scope disposed too early? The
usingblock must encompass all async operations:// Wrong: scope is disposed before async completesusing (TenantScope.BeginScope("tenant-a")){_ = Task.Run(async () => await DoWorkAsync()); // fire-and-forget}// DoWorkAsync runs after scope is disposed// Right: await within the scopeusing (TenantScope.BeginScope("tenant-a")){await DoWorkAsync();} -
Are you using
Task.Runornew Thread?AsyncLocal<T>flows throughasync/awaitcontinuations andTask.Run, but does NOT flow into rawThreadinstances. UseTask.Runor theawaitpattern. -
Nested scopes conflicting? If you nest
TenantScope.BeginScopecalls, the inner scope overrides the outer one. On dispose, the outer scope is restored. Verify the nesting order is correct.
DI Registration Conflicts
Section titled “DI Registration Conflicts”Multiple tenant context or resolver registrations causing unexpected behavior.
Checklist
Section titled “Checklist”-
SG default vs explicit registration. The SG registers
AddPragmaticMultiTenancy()(single-tenant default).app.UseMultiTenancy(...)calls the same method again, overriding the resolver. This is by design (DI last-registration-wins). -
ITenantContextregistered as singleton.ITenantContextmust be scoped (per-request). If it is registered as singleton, all requests share the same context — a cross-tenant data leak. The default registration is scoped. Check for manual singleton registrations. -
Multiple
UseMultiTenancycalls. Each call overrides the previous resolver registration. Only the last call wins. Use a singleUseMultiTenancycall with a chained builder for fallback:// Wrong: two separate callsapp.UseMultiTenancy(mt => mt.UseHeader());app.UseMultiTenancy(mt => mt.UseClaim()); // this replaces header// Right: one call with chainapp.UseMultiTenancy(mt => mt.UseHeader().UseClaim());
Middleware Ordering Issues
Section titled “Middleware Ordering Issues”Tenant resolution fails or produces wrong results due to middleware ordering.
Checklist
Section titled “Checklist”-
Authentication before tenant resolution.
ClaimTenantResolverreadsHttpContext.User. IfUseAuthentication()runs afterTenantResolutionMiddleware, claims are empty. Order:UseAuthentication()TenantResolutionMiddleware <-- must be after authUseRouting() -
With
PragmaticApp.RunAsync. The SG-generated host wiring places the middleware automatically in the correct position. You should not need to add it manually.
TenantId Not Set on New Entities
Section titled “TenantId Not Set on New Entities”Entities are created but TenantId is empty or null.
Checklist
Section titled “Checklist”-
Are you using the Pragmatic persistence layer? The auto-set behavior is part of the persistence pipeline. If you are using raw
DbContext.Add()ordb.Set<T>().Add(), the tenant is not set automatically. -
Is
ITenantContextresolved? The persistence layer readsITenantContext.TenantIdto set the entity’sTenantId. If the context is not resolved (null), the entity gets an emptyTenantId. -
Is the entity created in a non-HTTP context without TenantScope? Use
TenantScope.BeginScope("tenant-id")before creating entities in background jobs, seed scripts, or tests.
Subdomain Resolution Returns Null on localhost
Section titled “Subdomain Resolution Returns Null on localhost”During development, SubdomainTenantResolver always returns null.
The resolver requires at least 3 domain segments (subdomain.domain.tld). localhost has 1 segment and localhost:5001 still has 1 segment. This is by design — localhost has no subdomain.
Use a different strategy for local development:
app.UseMultiTenancy(mt =>{ if (builder.Environment.IsDevelopment()) mt.UseHeader(); // Use X-Tenant-Id header in dev else mt.UseSubdomain(); // Use subdomain in production});Or configure a local domain in your hosts file:
127.0.0.1 acme.myapp.local127.0.0.1 contoso.myapp.localCross-Tenant Data Leak in Raw Queries
Section titled “Cross-Tenant Data Leak in Raw Queries”Data from other tenants is returned when using raw SQL or direct DbContext queries.
TenantFilter is applied by the Pragmatic query pipeline (repositories, query methods). If you bypass the pipeline with raw SQL (db.Database.SqlQueryRaw(...)) or direct IQueryable access (db.Set<Invoice>()), the filter is not applied.
Always use the Pragmatic repository abstraction for tenant-scoped entities:
// Safe: TenantFilter appliedvar invoices = await repository.QueryAsync(ct);
// NOT safe: bypasses TenantFiltervar invoices = await dbContext.Invoices.ToListAsync(ct);If raw SQL is unavoidable, add the tenant filter manually:
var tenantId = tenantContext.TenantId;var invoices = await db.Database.SqlQueryRaw<Invoice>( "SELECT * FROM Invoices WHERE TenantId = {0}", tenantId).ToListAsync(ct);CompositeTenantResolver Logging Too Many Warnings
Section titled “CompositeTenantResolver Logging Too Many Warnings”The logs show frequent warnings from CompositeTenantResolver.
When multiple resolvers are chained and the first resolvers return null (expected behavior for fallback chains), the composite resolver still tries all of them. If the last resolver also returns null, it logs "No tenant resolver succeeded out of N registered resolver(s)".
-
Add a
SingleTenantResolveras the last fallback. This ensures at least one resolver always succeeds:app.UseMultiTenancy(mt => mt.UseHeader().UseClaim().UseSingleTenant("default") // always succeeds); -
Adjust log levels. The warning is expected when no resolver matches (e.g., anonymous/unauthenticated requests). If this is expected behavior, adjust the log level for
Pragmatic.MultiTenancy.CompositeTenantResolvertoErrororNonein your logging configuration.
Frequently Asked Questions
Section titled “Frequently Asked Questions”Can I use different resolution strategies per endpoint?
Section titled “Can I use different resolution strategies per endpoint?”Not directly. The resolution strategy is global (registered in DI). However, you can use a custom ITenantResolver that inspects the request path and delegates to different strategies based on the endpoint.
Can I disable tenant filtering for admin queries?
Section titled “Can I disable tenant filtering for admin queries?”Use IQueryFilterToggle to temporarily disable specific filters. This is part of Pragmatic.Persistence, not the multi-tenancy module.
Does TenantFilter apply to GetByIdAsync?
Section titled “Does TenantFilter apply to GetByIdAsync?”Yes. TenantFilter is applied to all queries executed through the Pragmatic repository, including GetByIdAsync. A tenant cannot access another tenant’s entity even with the correct ID.
What happens when TenantId is null on an ITenantEntity?
Section titled “What happens when TenantId is null on an ITenantEntity?”The entity would not match any tenant filter. It is effectively orphaned. Always ensure TenantId is set — the persistence layer does this automatically on insert.
Can I change the TenantFilter priority?
Section titled “Can I change the TenantFilter priority?”Not directly. The SG generates it with priority 200. If you need custom ordering, you can disable the SG filter (do not implement ITenantEntity) and write your own IQueryFilter<T> with a custom priority.
Getting Help
Section titled “Getting Help”- Check the logs. Enable
Debuglevel forPragmatic.MultiTenancyto see resolution results. - Inspect generated code. Look at
{Entity}.TenantFilter.g.csin theobj/directory to verify the filter is generated correctly. - Verify DI registrations. Use
services.BuildServiceProvider().GetService<ITenantContext>()in a test to confirm the registration chain. - Read the guides. See Concepts, Getting Started, and Common Mistakes.