TenancyKit.EntityFrameworkCore
1.0.3
dotnet add package TenancyKit.EntityFrameworkCore --version 1.0.3
NuGet\Install-Package TenancyKit.EntityFrameworkCore -Version 1.0.3
<PackageReference Include="TenancyKit.EntityFrameworkCore" Version="1.0.3" />
<PackageVersion Include="TenancyKit.EntityFrameworkCore" Version="1.0.3" />
<PackageReference Include="TenancyKit.EntityFrameworkCore" />
paket add TenancyKit.EntityFrameworkCore --version 1.0.3
#r "nuget: TenancyKit.EntityFrameworkCore, 1.0.3"
#:package TenancyKit.EntityFrameworkCore@1.0.3
#addin nuget:?package=TenancyKit.EntityFrameworkCore&version=1.0.3
#tool nuget:?package=TenancyKit.EntityFrameworkCore&version=1.0.3
TenancyKit
TenancyKit is a .NET 8 library for building multi-tenant applications with explicit tenant ownership in the domain model. It is inspired by Finbuckle.MultiTenant, but keeps the tenant id visible in your entities instead of using EF Core shadow properties.
The library resolves a tenant identifier from the current request, loads tenant information from a store, keeps the current tenant in an execution context, and integrates with Entity Framework Core to apply query filters, populate tenant ids during SaveChanges, and prevent cross-tenant write operations.
Portuguese documentation is available in README.pt-BR.md.
Architecture
The solution is split into small packages so consumers can reference only the pieces they need:
| Project | Responsibility |
|---|---|
TenancyKit.Abstractions |
Lightweight contracts such as tenant info, resolver, store, context, and accessor. |
TenancyKit.Core |
Options, tenant context, missing-tenant behavior, entity configuration, cache decorator, tenant id conversion, and standardized messages. |
TenancyKit.AspNetCore |
DI, middleware, header/query string/claims/host resolvers, in-memory store, HTTP store, and memory cache configuration helpers. |
TenancyKit.EntityFrameworkCore |
EF Core query filters, SaveChanges tenant id assignment, cross-tenant write protection, and EF Core tenant store. |
TenancyKit.Sample |
Runnable sample organized as a small Clean Architecture + DDD-style application. |
TenancyKit.Tests |
Unit, store, resolver, EF Core, and HTTP end-to-end tests. |
Runtime flow:
HTTP request
-> tenant resolver
-> tenant store
-> optional tenant lookup cache
-> tenant context
-> ASP.NET Core pipeline
-> EF Core query filters and SaveChanges handling
Core Concepts
Tenant metadata is represented by ITenantInfo.
public sealed class AppTenant : ITenantInfo
{
public string Id { get; set; } = string.Empty;
public string Identifier { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
Id is the value used to isolate data. Identifier is the value resolved from the request, such as a header value, query string value, claim, or subdomain.
Domain entities can use any tenant id property type that can be converted from/to string. Internally, the library compares tenant ids as strings using ordinal, case-sensitive comparison.
public interface ITenantProject
{
Guid TenantId { get; set; }
}
The library does not create shadow properties. The tenant id remains part of your domain model.
MissingTenantBehavior must be configured explicitly:
Throw: reject requests without a resolved/found tenant and throw when saving tenant-owned entities without a tenant.Ignore: allow execution to continue without a tenant. Use this carefully for public or non-tenant endpoints.
ASP.NET Core Usage
Register multi-tenancy in Program.cs:
builder.Services.AddTenancyKit<AppTenant>(options =>
{
options.UseMissingTenantBehavior(MissingTenantBehavior.Throw);
options.UseHeaderTenantResolver(); // X-Tenant
options.UseQueryStringTenantResolver(); // ?tenant=acme
options.UseClaimsTenantResolver(); // tid claim
options.UseHostTenantResolver(); // acme.example.com -> acme
options.UseEfCoreStore<AppTenant, AppDbContext>();
options.UseMemoryCacheTenantStore(cache =>
cache.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10));
options.ConfigureEntity<ITenantProject, Guid>(project => project.TenantId);
});
Add authentication before multi-tenancy if you use claim-based resolution:
app.UseAuthentication();
app.UseMultiTenancy<AppTenant>();
app.UseAuthorization();
Access the current tenant through ITenantContextAccessor<TTenantInfo>:
app.MapGet("/tenant", (ITenantContextAccessor<AppTenant> accessor) =>
{
return accessor.Current.Tenant;
});
Tenant Resolvers
Resolvers run in the order they are configured. The first non-empty identifier wins.
options.UseHeaderTenantResolver("X-Tenant");
options.UseQueryStringTenantResolver("tenant");
options.UseClaimsTenantResolver("tid");
options.UseHostTenantResolver();
The host resolver uses the first subdomain by default: acme.example.com resolves acme. A host without a subdomain, such as example.com, does not resolve a tenant.
You can add custom resolvers by implementing ITenantResolver:
public sealed class CustomTenantResolver : ITenantResolver
{
public Task<string?> ResolveAsync(
TenantResolutionContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult<string?>("acme");
}
}
Tenant Stores and Cache
The library includes several store options:
options.UseInMemoryStore(tenants);
options.UseEfCoreStore<AppTenant, AppDbContext>();
options.UseHttpTenantStore<AppTenant>(new Uri("https://tenants.example.com/api/tenants/"));
The HTTP store uses this contract:
GET {baseAddress}/{identifier}
It returns null for 404 Not Found and deserializes successful JSON responses to TTenantInfo.
Add memory caching around the currently configured store:
options.UseEfCoreStore<AppTenant, AppDbContext>();
options.UseMemoryCacheTenantStore(cache =>
cache.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5));
UseMemoryCacheTenantStore must be called after a store is configured.
Entity Framework Core Usage
Register the interceptor and add it to your DbContext options:
builder.Services.AddScoped<TenancyKitSaveChangesInterceptor<AppTenant>>();
builder.Services.AddDbContext<AppDbContext>((provider, options) =>
{
options
.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<TenancyKitSaveChangesInterceptor<AppTenant>>());
});
Apply multi-tenancy in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AppTenant>().HasKey(tenant => tenant.Id);
modelBuilder.ApplyMultiTenancy(_tenancyKitOptions, _tenantContextAccessor);
}
When using UseEfCoreStore<TTenantInfo, TDbContext>(), the tenant info type must be mapped in the selected DbContext.
Entities implementing configured interfaces receive automatic query filters, and the SaveChanges interceptor provides the following protections:
- Create: the tenant id is set automatically from the current tenant context. If a tenant id is already set and matches the current tenant, the value is preserved. If it differs from the current tenant, an exception is thrown.
- Update: the original tenant id is compared against the current tenant. If they differ, an exception is thrown, preventing cross-tenant updates.
- Delete: the original tenant id is compared against the current tenant. If they differ, an exception is thrown, preventing cross-tenant deletes.
When no tenant is available in the context and MissingTenantBehavior is Throw, any write operation involving a tenant-owned entity throws an exception.
Bypassing Tenant Filters
To bypass tenant query filters for a specific query (e.g., admin or back-office scenarios), use the built-in EF Core method .IgnoreQueryFilters():
var allProjects = await context.Projects
.IgnoreQueryFilters()
.ToListAsync();
Note:
.IgnoreQueryFilters()is an Entity Framework Core feature, not a TenancyKit feature. It removes all query filters defined on the entity type, not just the tenant filter. TheSaveChangesinterceptor cross-tenant write protection still applies even when query filters are bypassed.
Exceptions and Messages
Runtime and validation messages are centralized in:
TenancyKitErrorMessagesTenancyKitValidationMessages
Important user-facing cases include:
- missing
MissingTenantBehavior; - missing resolver or store configuration;
- unresolved tenant;
- tenant not found;
- saving tenant-owned entities without a current tenant;
- setting a tenant id on a newly added entity that does not match the current tenant;
- attempting to update or delete an entity that belongs to a different tenant;
- invalid tenant id expressions or non-writable tenant id properties.
These messages are documented with XML summaries and are reused by exceptions thrown by the library.
Sample Structure
TenancyKit.Sample is organized as a small Clean Architecture + DDD-style sample:
Domain
-> Project, ITenantProject, SampleTenantInfo
Application
-> DTOs, project service, repository contract
Infrastructure
-> SampleDbContext, EF repository, data seeding
Api
-> service registration and endpoint mapping
The sample uses EF InMemory to stay self-contained, but demonstrates the EF Core tenant store, memory cache, middleware, resolvers, query filters, and save interceptor.
Verification
Run:
dotnet build
dotnet test
dotnet pack -c Release
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.26)
- TenancyKit.Core (>= 1.0.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TenancyKit.EntityFrameworkCore:
| Package | Downloads |
|---|---|
|
TenancyKit
Meta-package for TenancyKit. Installs all packages needed for multi-tenancy support: Abstractions, Core, ASP.NET Core, and Entity Framework Core integrations. |
GitHub repositories
This package is not used by any popular GitHub repositories.