Troubleshooting
Common Tenantry.Pro pitfalls and how to diagnose them. For how tenants are resolved and stored (the Tenantry core layer), see the Tenantry core troubleshooting guide.
LicenseRequiredException is thrown
The application stopped at startup, or a licence-guarded operation (provisioning, migration orchestration, the lifecycle pipeline) refused to run, because the licence key is missing or invalid. The message says which; for an invalid key, the log says why it was rejected.
- No key configured — call
pro.WithLicence(builder.Configuration["Tenantry:Licence"]!)and confirm the configuration value is actually present in this environment (user secrets locally, an environment variable or secret in CI and production). - Malformed, tampered or wrong key — the JWT failed signature or issuer validation. Copy the key again from your Pro access page, in full: it is one three-segment token.
Keys do not expire, so a key that worked keeps working. Read-only MigrationStatusTracker never throws
this — use it for monitoring regardless of licence state. See Licensing.
TenantNotResolvedException when resolving a connection string
ITenantConnectionStringResolver<TKey>.Resolve() was called with no tenant in scope.
- In ASP.NET Core, ensure
app.UseTenantry()runs before anything that resolves a connection string, and that the request actually carries a resolvable tenant (e.g. theX-Tenant-Idheader). - In a worker/console host there is no request — open a scope first with
ITenantScopeFactory<TKey>. See Background jobs & non-HTTP hosts.
Resolve() throws but ResolveAsync() works
You configured only GetConnectionStringAsync. The synchronous Resolve() cannot run an async
delegate — call ResolveAsync(ct), or also set a synchronous GetConnectionString.
All tenants see the same data under schema-per-tenant
EF Core cached one compiled model (the first tenant's schema) for the whole context type. Add
options.AddSchemaPerTenantCaching<TKey>(sp) in your AddDbContext factory so EF Core keeps a
separate model per schema, and make sure OnModelCreating calls HasDefaultSchema(...) with the
resolved schema. See Schema per tenant.
AddSchemaProvisioning does not compile / connection string is ignored
AddSchemaProvisioning now requires a configure delegate, and the shared-database connection
string lives on its options — not on UseSchemaPerTenant:
pro.UseSchemaPerTenant(opts => opts.GetSchemaName = t => $"tenant_{t.TenantId}");
pro.AddSchemaProvisioning(opts => opts.ConnectionString = connectionString);SchemaPerTenantOptions has no ConnectionString property.
WithMigrationOrchestration throws at startup
It throws immediately if UseDatabasePerTenant(...) was not called first on the same builder. Order
the calls so UseDatabasePerTenant precedes WithMigrationOrchestration. Pass both type arguments:
WithMigrationOrchestration<TKey, TContext>(...).
A captive-dependency error mentioning ITenantStore
A singleton tried to inject the (scoped) tenant store directly. Inject
ITenantStoreAccessor<TKey> instead — it resolves the store from a fresh scope per call. This is the
correct dependency for hosted services and any custom singleton that enumerates tenants. See
Background jobs & non-HTTP hosts.
Trim/AOT analyzer warnings (IL2026, IL3050)
Expected where you call the EF Core features: WithMigrationOrchestration, AddAuditLogging, the
migration status tracker and the migration health check. EF Core relies on reflection and runtime code
generation, so these APIs are annotated [RequiresDynamicCode]/[RequiresUnreferencedCode] and are
not Native AOT-compatible; schema-per-tenant model caching is EF Core configuration and has EF Core's
own limits. Tenantry.Pro and Tenantry.Pro.AspNetCore (licensing, the strategies, caching and
encryption, the lifecycle pipeline, telemetry) are trim- and AOT-compatible: an app over them publishes
with PublishAot without warnings. The lifecycle pipeline runs migrations only when
WithMigrationOrchestration registered them, so its warning appears at that call. If you publish with
PublishTrimmed/PublishAot, leave the EF Core features out, or suppress the warnings where you have
accepted the constraint.
Per-tenant metrics are missing or all show tenant.id = unknown
- Ensure
app.UseTenantryMetrics()runs afterapp.UseTenantry(), so the tenant context is populated before a metric is recorded. unknownmeans no tenant was resolved for that request — expected for unauthenticated or pre-resolution endpoints. Excluded paths (default/health,/healthz,/ready) emit nothing.- Confirm your collector subscribes to the meter named
Tenantry.Pro. See Telemetry.
A background job / consumer runs without a tenant
Jobs enqueued or messages published outside a tenant scope carry no tenant, so by default they run
without one (logging a warning). Stamp/enqueue from within an active scope, or check
ITenantContext<TKey>.HasTenant inside the job and handle the scopeless case.
To change what happens when a job/message has no resolvable tenant, set the missing-tenant policy on
the integration's registration — e.g. pro.AddHangfireTenantFilter(o => o.OnMissingTenant = MissingTenantBehavior.Reject). Allow runs scopeless silently, Warn (default) runs scopeless and
logs, Reject throws (the host's retry/error handling takes over), and Skip drops the job/message
without running it. This mirrors core's EfCoreIsolationOptions.OnMissingTenant. See
Hangfire, MassTransit, Quartz.NET, Rebus,
and Background jobs.