CleanArchitecture.Extensions.Multitenancy.EFCore
0.2.9
dotnet add package CleanArchitecture.Extensions.Multitenancy.EFCore --version 0.2.9
NuGet\Install-Package CleanArchitecture.Extensions.Multitenancy.EFCore -Version 0.2.9
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.EFCore" Version="0.2.9" />
<PackageVersion Include="CleanArchitecture.Extensions.Multitenancy.EFCore" Version="0.2.9" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.EFCore" />
paket add CleanArchitecture.Extensions.Multitenancy.EFCore --version 0.2.9
#r "nuget: CleanArchitecture.Extensions.Multitenancy.EFCore, 0.2.9"
#:package CleanArchitecture.Extensions.Multitenancy.EFCore@0.2.9
#addin nuget:?package=CleanArchitecture.Extensions.Multitenancy.EFCore&version=0.2.9
#tool nuget:?package=CleanArchitecture.Extensions.Multitenancy.EFCore&version=0.2.9
CleanArchitecture.Extensions.Multitenancy.EFCore
EF Core adapters for tenant-aware data isolation in the JaysonTaylorCleanArchitectureBlank template. This package adds model filters, SaveChanges enforcement, schema helpers, and tenant-aware DbContext primitives.
Step 1 - Install the package
Install in the Infrastructure project:
dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Multitenancy.EFCore
Step 2 - Register EF Core multitenancy services
File: src/Infrastructure/DependencyInjection.cs
using CleanArchitecture.Extensions.Multitenancy.EFCore;
using CleanArchitecture.Extensions.Multitenancy.EFCore.Interceptors;
using CleanArchitecture.Extensions.Multitenancy.EFCore.Options;
using Microsoft.EntityFrameworkCore;
builder.Services.AddCleanArchitectureMultitenancyEfCore(options =>
{
options.Mode = TenantIsolationMode.SharedDatabase;
options.TenantIdPropertyName = "TenantId";
options.UseShadowTenantId = true;
});
builder.Services.AddDbContext<ApplicationDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("JaysonTaylorCleanArchitectureBlankDb"));
options.AddInterceptors(sp.GetRequiredService<TenantSaveChangesInterceptor>());
options.UseTenantModelCacheKeyFactory(sp);
});
Call options.UseTenantModelCacheKeyFactory(sp) when configuring your DbContext to enable tenant-aware model cache keys. If you need a custom IModelCacheKeyFactory, call ReplaceService after this line.
If you already register ISaveChangesInterceptor instances (as the template does), you can omit the explicit TenantSaveChangesInterceptor line and rely on GetServices<ISaveChangesInterceptor>().
Row-level filtering/enforcement defaults to shared database mode. For schema/database-per-tenant setups, set UseShadowTenantId, EnableQueryFilters, and EnableSaveChangesEnforcement to true if you want defense-in-depth at the row level.
Step 3 - Wire tenant context into your DbContext
Option A: use the tenant-aware base class (non-Identity DbContext)
using CleanArchitecture.Extensions.Multitenancy.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.EFCore;
public sealed class ApplicationDbContext : TenantDbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options,
ICurrentTenant currentTenant,
IOptions<EfCoreMultitenancyOptions> optionsAccessor,
ITenantModelCustomizer modelCustomizer)
: base(options, currentTenant, optionsAccessor, modelCustomizer)
{
}
}
Option B: keep IdentityDbContext (template default)
The Jason Taylor template uses IdentityDbContext, so you can implement ITenantDbContext directly and apply the model customizer after your configurations.
using System.Reflection;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.EFCore.Options;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, ITenantDbContext
{
private readonly ICurrentTenant _currentTenant;
private readonly EfCoreMultitenancyOptions _multitenancyOptions;
private readonly ITenantModelCustomizer _tenantModelCustomizer;
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options,
ICurrentTenant currentTenant,
IOptions<EfCoreMultitenancyOptions> optionsAccessor,
ITenantModelCustomizer tenantModelCustomizer)
: base(options)
{
_currentTenant = currentTenant;
_multitenancyOptions = optionsAccessor.Value;
_tenantModelCustomizer = tenantModelCustomizer;
}
public string? CurrentTenantId => _currentTenant.TenantId;
public ITenantInfo? CurrentTenantInfo => _currentTenant.TenantInfo;
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
_tenantModelCustomizer.Customize(builder, this, _multitenancyOptions);
}
}
Note: if you add your own HasQueryFilter calls, apply tenant filtering last by calling ApplyTenantModel(builder) (from TenantDbContext) or _tenantModelCustomizer.Customize(...) after your configurations.
Step 4 - Mark tenant entities
Use explicit tenant identifiers or let the extension add a shadow property:
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
public sealed class Customer : ITenantEntity
{
public int Id { get; set; }
public string TenantId { get; set; } = string.Empty;
}
If you already use BaseAuditableEntity, you can either:
- Keep it as-is and rely on the shadow
TenantIdproperty. - Implement
ITenantEntityon your base entity to make the tenant column explicit.
To exclude global entities from filtering:
using CleanArchitecture.Extensions.Multitenancy.EFCore.Abstractions;
[GlobalEntity]
public sealed class FeatureFlag
{
public int Id { get; set; }
}
Identity entities
By default, ASP.NET Core Identity entities are treated as global to keep the template working without extra changes. Set TreatIdentityEntitiesAsGlobal = false if you want tenant-scoped identity and add a tenant identifier to your Identity entities or custom stores.
Schema-per-tenant setup
builder.Services.AddCleanArchitectureMultitenancyEfCore(options =>
{
options.Mode = TenantIsolationMode.SchemaPerTenant;
options.SchemaNameFormat = "tenant_{0}";
});
Database-per-tenant setup
builder.Services.AddCleanArchitectureMultitenancyEfCore(options =>
{
options.Mode = TenantIsolationMode.DatabasePerTenant;
options.ConnectionStringFormat = "Server=.;Database=Tenant_{0};Trusted_Connection=True;TrustServerCertificate=True;";
});
builder.Services.AddDbContextFactory<ApplicationDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("JaysonTaylorCleanArchitectureBlankDb"));
});
builder.Services.AddTenantDbContextFactory<ApplicationDbContext>();
For request-scoped DbContext registration, resolve the tenant connection inside AddDbContext:
builder.Services.AddDbContext<ApplicationDbContext>((sp, options) =>
{
var currentTenant = sp.GetRequiredService<ICurrentTenant>();
var resolver = sp.GetRequiredService<ITenantConnectionResolver>();
var connectionString = resolver.ResolveConnectionString(currentTenant.TenantInfo);
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("Tenant connection string was not resolved.");
}
options.UseSqlServer(connectionString);
});
ITenantDbContextFactory<TContext> switches the connection string per tenant for background tasks and migrations. Register a custom resolver if you want to pull connection strings from a vault or catalog.
What to expect
- Shared database mode automatically adds a tenant filter (
TenantId) to all tenant-scoped entities. - SaveChanges enforces tenant ownership and rejects cross-tenant updates.
- Schema-per-tenant mode applies the tenant schema and isolates EF Core model caching when
UseTenantModelCacheKeyFactory(sp)is enabled. - Global entities are excluded when they implement
IGlobalEntityor use[GlobalEntity].
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- CleanArchitecture.Extensions.Multitenancy (>= 0.2.9)
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.