Tenantry documentation
Tenantry is a flexible, modern, and unopinionated multi-tenancy library for .NET. It isolates each
tenant's data in a shared database using a TenantId column, wiring the isolation in through an
EF Core interceptor and global query filters, or gives each tenant its own database through
per-tenant connection strings (database per tenant) —
without forcing a base class on your entities or taking over your request pipeline. Schema-per-tenant,
provisioning and migrations across tenant databases are in Tenantry.Pro.
If you are new, start with Getting started and Core concepts.
Guides
- Getting started — install the packages and build a tenant-aware app end to end.
- Core concepts — the tenant key,
ITenantDescriptor,ITenantContextvs.ITenantScope, and theAsyncLocalmodel that ties them together. - Tenant stores — the in-memory store, writing a custom
ITenantStore, and service lifetimes. - ASP.NET Core integration —
AddTenantry, the resolution middleware, pipeline ordering, and HTTP status codes. - Tenant resolution — header, subdomain, route, claim, and query-string resolvers, resolver ordering, and custom resolvers.
- Access control — requiring tenants per-endpoint or globally, access validators, and claim-based validation.
- EF Core integration — query filters, the
SaveChangesinterceptor, the isolation policy, the optional base context, pooling, a database per tenant, migrations, and admin/cross-tenant queries. - Non-HTTP hosts —
AddTenantryCorefor console apps, worker services, and background jobs. - AOT & trimming — exactly what is supported, per package, and why EF Core differs.
- Compatibility — supported .NET and EF Core versions, databases, and dependency ranges.
- Troubleshooting — common pitfalls and how to diagnose them.
- API reference — every public type and member, generated from the XML documentation comments.
How the pieces fit together
Tenantry has three responsibilities, each configured in the AddTenantry/AddTenantryCore lambda:
| Responsibility | Question it answers | Configured with |
|---|---|---|
| Resolution | Who is the tenant for this request/operation? | ResolveFromHeader(...), ResolveFromClaim(...), … (ASP.NET Core), or a manual BeginScope(...) (non-HTTP) |
| Storage | Which tenants exist, and what are their details? | UseInMemoryStore(...), UseStore<T>() |
| Isolation | How is each tenant's data kept separate? | AddEfCoreIsolation(...), plus UseConnectionStrings(...) for a database per tenant |
The flow on an ASP.NET Core request:
HTTP request
│
▼
UseTenantry() ──► resolver(s) extract a raw id ──► parse to TKey ──► ITenantStore looks it up
│ │
│ (optional) access validators run │
▼ ▼
ITenantScope.BeginScope(tenant) sets the AsyncLocal tenant for the rest of the request
│
▼
Your endpoint + EF Core
├─ reads ──► global query filter restricts rows to ITenantContext.CurrentTenantId
└─ writes ──► SaveChanges interceptor stamps/validates TenantIdIn a console or worker app there is no request, so you open the scope yourself with
ITenantScopeFactory (or BeginScope); everything below that line behaves identically.