TenancyKit.EntityFrameworkCore 1.0.3

dotnet add package TenancyKit.EntityFrameworkCore --version 1.0.3
                    
NuGet\Install-Package TenancyKit.EntityFrameworkCore -Version 1.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TenancyKit.EntityFrameworkCore" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TenancyKit.EntityFrameworkCore" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="TenancyKit.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TenancyKit.EntityFrameworkCore --version 1.0.3
                    
#r "nuget: TenancyKit.EntityFrameworkCore, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TenancyKit.EntityFrameworkCore@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TenancyKit.EntityFrameworkCore&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=TenancyKit.EntityFrameworkCore&version=1.0.3
                    
Install as a Cake Tool

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. The SaveChanges interceptor cross-tenant write protection still applies even when query filters are bypassed.

Exceptions and Messages

Runtime and validation messages are centralized in:

  • TenancyKitErrorMessages
  • TenancyKitValidationMessages

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.0.3 81 5/5/2026
1.0.2 86 5/3/2026
1.0.1 88 5/3/2026