EricksonLopez.MultiTenancy.OpenTelemetry
2.0.0
dotnet add package EricksonLopez.MultiTenancy.OpenTelemetry --version 2.0.0
NuGet\Install-Package EricksonLopez.MultiTenancy.OpenTelemetry -Version 2.0.0
<PackageReference Include="EricksonLopez.MultiTenancy.OpenTelemetry" Version="2.0.0" />
<PackageVersion Include="EricksonLopez.MultiTenancy.OpenTelemetry" Version="2.0.0" />
<PackageReference Include="EricksonLopez.MultiTenancy.OpenTelemetry" />
paket add EricksonLopez.MultiTenancy.OpenTelemetry --version 2.0.0
#r "nuget: EricksonLopez.MultiTenancy.OpenTelemetry, 2.0.0"
#:package EricksonLopez.MultiTenancy.OpenTelemetry@2.0.0
#addin nuget:?package=EricksonLopez.MultiTenancy.OpenTelemetry&version=2.0.0
#tool nuget:?package=EricksonLopez.MultiTenancy.OpenTelemetry&version=2.0.0
EricksonLopez.MultiTenancy
Secure, high-performance, Native AOT-compatible, enterprise-grade multi-tenancy ecosystem and 4-layer Defense-in-Depth isolation for modern .NET.
EricksonLopez.MultiTenancy is a foundational, Native AOT-first multi-tenancy ecosystem engineered for .NET 8.0 (LTS), .NET 9.0 (STS), and .NET 10.0 applications built with Clean Architecture, Domain-Driven Design (DDD), and relational persistence engines (PostgreSQL, Microsoft SQL Server, MySQL, MariaDB, Oracle, and SQLite).
The architecture is founded upon an absolute, non-negotiable security invariant:
A single defective layer must not be able to destroy tenant isolation (Defense-in-Depth).
By coupling explicit application parameterization, scoped write-once accessors, and transaction-scoped database session contexts (SET LOCAL for PostgreSQL Row Level Security, sp_set_session_context for SQL Server), the ecosystem eliminates ambient state leakage across recycled connection pools, rejects brittle dynamic SQL string rewriting, enforces compile-time Roslyn diagnostic rules, and guarantees zero heap allocations on identifier comparisons and resolution hot paths.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- 1. Clean Architecture & CQRS Query Handlers
- 2. Multi-Strategy Resolution with Fail-Closed Conflict Detection
- 3. Per-Tenant Configuration & Feature Options
- 4. Background Job & Message Queue Consumers
- 5. Per-Tenant Authentication Schemes & Dynamic Cookies
- 6. Database-per-Tenant Dynamic Connection Routing
- Configuration & Integrations
- Testing & Quality
- Performance Benchmarks
- Compatibility & Technical Matrix
- Architecture & Design Principles
- Best Practices & Anti-Patterns
- Troubleshooting & Common Pitfalls
- Part of the Ecosystem
- Contributing
- License
๐ฏ What Problem It Solves
Traditional Multi-Tenancy Anti-Patterns
- The Fragile Illusion of Application-Only Filtering:
Legacy libraries rely exclusively on runtime AST/regex SQL string rewriting or Entity Framework Core Global Query Filters (
HasQueryFilter). If an engineer executes a raw Dapper query, uses a subquery, joins an un-mapped view, or invokes a native stored procedure, the filter is omitted and tenant isolation collapses silently. - Connection Pool Contamination & Session Bleeding:
Setting session-level database state (
SET app.current_tenant = 'tenant-a') binds state to the physical connection. When ADO.NET returns the connection to the connection pool, subsequent requests for Tenant B reusing that connection inherit Tenant A's session context, triggering catastrophic cross-tenant data leaks. - Ambient State Bleeding & Captive Singletons:
Storing mutable tenant context in static
AsyncLocal<T>slots causes ambient context leakage across un-awaited tasks and background worker threads. Furthermore, resolving scoped tenant accessors inside Singleton-lifetime services creates captive dependencies, permanently freezing the tenant context of the first request that hit the server. - Tenant Identifier Spoofing via Ambiguous Resolution:
Allowing unauthenticated HTTP headers (
X-Tenant-ID) to silently override cryptographically validated JWT claims allows malicious actors to escalate privileges and access unauthorized tenant partitions. - Memory Allocation Overhead & Reflection Bottlenecks:
Representing tenant identifiers as generic heap strings (
string TenantId) causes continuous heap allocations, garbage collection pressure, and hash collisions. Relying on heavy runtime reflection prevents modern compilation targets such as Native AOT and assembly trimming.
How EricksonLopez.MultiTenancy Solves This
- ๐ก๏ธ 4-Layer Defense-in-Depth: Application-level parameterization (
WHERE tenant_id = @TenantId), scoped accessor validation, transaction-scoped database session binding, and database-level Row Level Security (RLS) operate collaboratively so no single code defect can breach tenant boundaries. - ๐ Transaction-Scoped Database Isolation (
SET LOCAL): Database context variables are bound strictly to the transaction lifecycle viaSET LOCAL(PostgreSQL) orsp_set_session_context(SQL Server). When a transaction completes (COMMITorROLLBACK), the database automatically purges the variable, returning clean connections to the pool. - ๐งฑ Immutable, Zero-Allocation Struct
TenantId: A 128-bitGuid-backed readonly record struct implementingIEquatable<TenantId>andIComparable<TenantId>with safe stack span parsing (TenantId.TryCreate) and zero heap boxing. - ๐ฆ Fail-Closed Resolution Precedence (ADR-008): Cryptographically verified JWT claims strictly supersede client-controlled data. If multiple resolution strategies yield conflicting tenant identifiers, the pipeline immediately fails closed โ emitting HTTP 409 Conflict (when
WriteProblemDetailsOnConflict = true) or throwingTenantResolutionConflictException(whenfalse). - ๐ต๏ธ Compile-Time Roslyn Analyzers: Analyzers
ELMT001,ELMT002, andELMT003intercept static context leaks, captive singleton injections, and un-scoped Dapper queries directly during compilation. - โก 100% Native AOT & Trimming Compliant: Zero runtime reflection, zero dynamic code generation (
IL.Emit), and explicit type registrations ensure full compatibility with ahead-of-time compilation.
โก Key Features
- ๐ก๏ธ 4-Layer Defense-in-Depth Architecture: Guarantees isolation even if application code fails to apply a WHERE filter.
- ๐ Zero-Allocation
TenantIdPrimitives: 128-bit stack struct with explicit UTF-8 JSON converters and span parsing. - ๐ Transaction-Scoped RLS Integration: Native adapters for PostgreSQL RLS, SQL Server
SESSION_CONTEXT, MySQL/MariaDB session variables, Oracle VPD, and SQLite DB-per-tenant. - โก Native AOT & Trimming Ready: Pre-configured with
<IsAotCompatible>true</IsAotCompatible>and verified via smoke test suites. - ๐ต๏ธ Dedicated Roslyn Static Analyzers: Built-in compiler rules (
ELMT001-ELMT003) enforcing lifecycle and DI safety. - ๐ฆ Fail-Closed Strategy Conflict Resolution: Automatic detection and rejection of conflicting tenant resolution vectors (ADR-008).
- ๐ Isolated Background Scope Factory:
ITenantScopeFactorycreates clean, isolated DI service scopes for non-HTTP background workers and message queue consumers. - ๐ช Per-Tenant Authentication & Options: Dynamic cookie event validation, per-tenant options caching, and scheme routing.
- ๐ Native OpenTelemetry Instrumentation: Distributed tracing (
TenantActivitySource), metrics (TenantMetrics), and W3C Baggage propagation. - ๐งช Comprehensive Test Doubles & Harnesses: Built-in
FakeTenantStore,FakeTenantResolutionStrategy, andTenantContextBuilderfor frictionless unit and integration testing.
๐ฆ Ecosystem
| Package | Version | Description |
|---|---|---|
EricksonLopez.MultiTenancy.Abstractions |
Foundational contracts: TenantId, ITenantInfo, ITenantContext, ITenantResolver, ITenantScope, and TenantErrors. |
|
EricksonLopez.MultiTenancy |
Core engine: ScopedTenantContextAccessor, DefaultTenantScopeFactory, InMemoryTenantStore, and CachedTenantStore. |
|
EricksonLopez.MultiTenancy.Analyzers |
Roslyn analyzers: static field leaks (ELMT001), singleton captivity (ELMT002), un-scoped Dapper queries (ELMT003). |
|
EricksonLopez.MultiTenancy.AspNetCore |
ASP.NET Core resolution middleware, JWT/Host/Route/Header strategies, .RequireTenant() endpoint filters. |
|
EricksonLopez.MultiTenancy.Authentication |
Per-tenant authentication schemes, cookie validation events, dynamic scheme routing. | |
EricksonLopez.MultiTenancy.Configuration |
IConfiguration and IOptionsMonitor-backed tenant store for file-based configuration. |
|
EricksonLopez.MultiTenancy.Dapper |
Dapper parameter builders and query parameter helpers (WithTenant, CreateTenantParameters). |
|
EricksonLopez.MultiTenancy.PostgreSql |
PostgreSQL Row Level Security (RLS) enforcement via transaction-scoped SET LOCAL + PostgreSqlTenantStore. |
|
EricksonLopez.MultiTenancy.SqlServer |
SQL Server SESSION_CONTEXT management via sp_set_session_context and security policies. |
|
EricksonLopez.MultiTenancy.MySql |
MySQL session variable isolation (@app_tenant_id) and connection scoping. |
|
EricksonLopez.MultiTenancy.MariaDb |
MariaDB session variable isolation (@app_tenant_id) and connection scoping. |
|
EricksonLopez.MultiTenancy.Oracle |
Oracle Virtual Private Database (VPD) and DBMS_SESSION.SET_IDENTIFIER integration. |
|
EricksonLopez.MultiTenancy.Sqlite |
SQLite database-per-tenant (ISqliteTenantConnectionFactory) and temp table session context. |
|
EricksonLopez.MultiTenancy.OpenTelemetry |
Distributed tracing (TenantActivitySource), W3C Baggage propagation, metrics (TenantMetrics). |
|
EricksonLopez.MultiTenancy.Testing |
Test doubles: FakeTenantStore, FakeTenantResolutionStrategy, TenantContextBuilder, FakeDbInfrastructure. |
๐ Documentation
๐ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-multitenancy/tree/main/docs
๐ Step-by-Step Interactive Showcase (Levels 00 to 08)
| Level | Topic | Description |
|---|---|---|
| Level 00 | Foundations & Architecture | Core architectural pillars, TenantId struct invariants, and zero context leakage |
| Level 01 | Getting Started & Registration | Minimal DI setup, in-memory store seeding, and core pipeline initialization |
| Level 02 | Resolution Strategies & Precedence | Claims, HostName, Route, BasePath, Header strategies, and fail-closed conflict detection |
| Level 03 | Database-per-Tenant Pattern | Dynamic SQLite connection factories and isolated database file routing |
| Level 04 | Shared Database & Row-Level Security | PostgreSQL SET LOCAL, SQL Server SESSION_CONTEXT, and MySQL/MariaDB isolation |
| Level 05 | ASP.NET Core & Endpoint Security | Middleware ordering, .RequireTenant() endpoint filters, and .AllowAnonymousTenant() |
| Level 06 | Per-Tenant Authentication | Tenant cookie authentication events, dynamic schemes, and cross-subdomain safety |
| Level 07 | Native AOT & Zero-Allocation | Ahead-of-time compilation, trimming annotations, and stack-allocated span parsing |
| Level 08 | Telemetry, Observability & Testing | OpenTelemetry ActivitySource, metrics, W3C Baggage, and test doubles harness |
๐ Runnable showcase project implementation: Showcase Sample Application
๐ Technical Reference & Architecture Guides
- System Architecture & Invariants โ Comprehensive architectural blueprint, DI lifetimes, package layering, and safety invariants.
- System Overview & Blueprint โ End-to-end request flow diagram and component interaction topology.
- Master Feature Matrix โ Module-by-module capability inventory, Native AOT verification, and dialect matrices.
- Strategic Matrices & Roadmap โ Detailed capability matrices, release gates checklist, and engineering roadmap.
- Public API Inventory โ Complete inventory of public types and contracts across all 15 packages.
- Architectural Decision Records (ADRs) โ Formal ADR-001 through ADR-012 plus Discard Records (ADR-D01 to ADR-D04).
- Roslyn Diagnostic Rules (
docs/rules/) โ Technical rule specifications forELMT001,ELMT002, andELMT003. - Benchmark Plan & Budgets โ Performance budgets, allocation limits, and benchmark suite architecture.
- Benchmark Results & Evidence โ Competitive benchmarks vs legacy reflection and ambient models.
- Quality Gates & DevSecOps โ Automated quality gates, SonarCloud, Stryker, and Native AOT policies.
- Testing & Mutation Audit Report โ Test inventory, mutation scores breakdown (100% kill rate), and ArchUnit rules.
- Security Threat Model (STRIDE) โ Threat model, attack vectors, precedence guarantees, and mitigations.
- PostgreSQL Row Level Security (RLS) โ Transaction-scoped
SET LOCALintegration,FORCE ROW LEVEL SECURITY, and restrictive policies. - Database Dialects Matrix โ Multi-database comparison across PostgreSQL, SQL Server, MySQL, MariaDB, Oracle, and SQLite.
- Background Processing Guide โ Scoping background jobs and message queue handlers with
ITenantScopeFactory. - Per-Tenant Authentication โ Per-tenant authentication schemes, options cache, and cookie security events.
- Performance & Native AOT Guide โ Zero-allocation struct designs, span parsing, and trimming compatibility.
- Testing Strategy & Quality Gates โ Test suite architecture, 100% coverage policy, and Stryker mutation testing gates.
- CI/CD & DevSecOps Pipelines โ Pipeline architecture, Sigstore SLSA provenance, and NuGet OIDC release automation.
- Best Practices & Roslyn Analyzers โ Coding standards and compiler diagnostic rules (
ELMT001-ELMT003). - Cookbook & Engineering Recipes โ 12 production-ready engineering recipes for enterprise scenarios.
- Troubleshooting & Diagnostics โ Detailed remediation for resolution conflicts, RLS issues, and captive dependencies.
- Feature Status Matrix โ Module-by-module feature status, central package management, and framework support.
- Architectural FAQ โ Frequently asked architectural and security questions.
- Migration Guide โ Step-by-step migration guide from legacy multi-tenancy packages.
- Competitive Functional Parity Audit โ In-depth architectural and functional comparison vs alternative libraries.
๐ฅ Installation
Core Engine & Abstractions
# Core abstractions, TenantId struct, and contracts (Layer 0)
dotnet add package EricksonLopez.MultiTenancy.Abstractions
# Core engine, scoped accessors, and scope factory (Layer 1)
dotnet add package EricksonLopez.MultiTenancy
# Compile-time Roslyn static analyzers (Layer 2)
dotnet add package EricksonLopez.MultiTenancy.Analyzers
Web & ASP.NET Core Hosting
# ASP.NET Core resolution middleware, strategies, and endpoint filters (Layer 3)
dotnet add package EricksonLopez.MultiTenancy.AspNetCore
# Per-tenant authentication schemes and dynamic cookie events (Layer 3)
dotnet add package EricksonLopez.MultiTenancy.Authentication
# IConfiguration and IOptionsMonitor-backed tenant store (Layer 3)
dotnet add package EricksonLopez.MultiTenancy.Configuration
Relational Database Dialects
# Dapper parameter builders and query helpers (Layer 4)
dotnet add package EricksonLopez.MultiTenancy.Dapper
# Choose your database dialect engine adapter (Layer 5):
dotnet add package EricksonLopez.MultiTenancy.PostgreSql
dotnet add package EricksonLopez.MultiTenancy.SqlServer
dotnet add package EricksonLopez.MultiTenancy.MySql
dotnet add package EricksonLopez.MultiTenancy.MariaDb
dotnet add package EricksonLopez.MultiTenancy.Oracle
dotnet add package EricksonLopez.MultiTenancy.Sqlite
Observability & Testing
# OpenTelemetry distributed tracing and metrics (Layer 6)
dotnet add package EricksonLopez.MultiTenancy.OpenTelemetry
# Testing doubles and unit testing harnesses (Layer 7)
dotnet add package EricksonLopez.MultiTenancy.Testing
๐ Quick Start
1. Strongly-Typed Tenant Identity
using EricksonLopez.MultiTenancy;
// 1. Immutable 128-bit readonly record struct backed by Guid
var tenantId = TenantId.Create("11111111-1111-1111-1111-111111111111");
// 2. Safe stack-allocated span parsing (Zero heap allocations)
if (TenantId.TryCreate("22222222-2222-2222-2222-222222222222", out var parsedId))
{
Console.WriteLine($"Successfully parsed: {parsedId}");
}
// 3. Domain tenant metadata entity
var tenantInfo = new TenantInfo(
id: tenantId,
name: "acme-corp",
isActive: true);
2. Dependency Injection & Pipeline Configuration
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// 1. Register core multi-tenancy engine
builder.Services.AddMultiTenancy();
// 2. Register ASP.NET Core resolution pipeline (JWT claims enabled by default)
builder.Services.AddAspNetCoreMultiTenancy();
// 3. Register additional resolution strategies (Precedence: Claims > Host > Route > Header)
builder.Services.AddHostNameTenantStrategy();
builder.Services.AddRouteTenantStrategy("tenantId");
// 4. Seed an in-memory tenant store for local development
builder.Services.AddInMemoryTenantStore<TenantInfo>(store =>
{
store.AddOrUpdate(new TenantInfo(
id: TenantId.Create("11111111-1111-1111-1111-111111111111"),
name: "acme",
isActive: true));
store.AddOrUpdate(new TenantInfo(
id: TenantId.Create("22222222-2222-2222-2222-222222222222"),
name: "globex",
isActive: true));
});
var app = builder.Build();
// 5. Activate resolution middleware early in the request pipeline
app.UseMultiTenancy();
// 6. Define endpoints guarded with .RequireTenant()
app.MapGet("/api/tenant-profile", (ITenantContext tenantContext) =>
{
var tenant = tenantContext.RequiredTenant;
return Results.Ok(new { Tenant = tenant.Name, Id = tenant.Id.ToString() });
}).RequireTenant();
app.Run();
3. Explicit SQL Queries with Dapper (Layer 1)
using System.Data.Common;
using Dapper;
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.Dapper;
public class InvoiceRepository
{
private readonly ITenantContext _tenantContext;
private readonly DbConnection _connection;
public InvoiceRepository(ITenantContext tenantContext, DbConnection connection)
{
_tenantContext = tenantContext;
_connection = connection;
}
public async Task<IEnumerable<Invoice>> GetInvoicesAsync()
{
// Explicit parameterization ensures Layer 1 Defense-in-Depth
var parameters = _tenantContext.CreateTenantParameters();
return await _connection.QueryAsync<Invoice>(
"SELECT * FROM invoices WHERE tenant_id = @TenantId",
parameters);
}
}
4. PostgreSQL Row Level Security Enforcement (Layers 3 & 4)
using Dapper;
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.PostgreSql;
using Npgsql;
public class SecureInvoiceService
{
private readonly NpgsqlConnection _connection;
private readonly ITenantContext _tenantContext;
public SecureInvoiceService(NpgsqlConnection connection, ITenantContext tenantContext)
{
_connection = connection;
_tenantContext = tenantContext;
}
public async Task ProcessOrdersAsync()
{
// Atomically opens connection and sets SET LOCAL app.current_tenant_id = :tenantId inside transaction
await using var transaction = await _connection.BeginTenantTransactionAsync(_tenantContext);
// Queries within this transaction are automatically filtered by PostgreSQL RLS
var invoices = await _connection.QueryAsync<Invoice>(
"SELECT * FROM invoices",
transaction: transaction);
await transaction.CommitAsync();
// Transaction completion automatically wipes session context โ zero state leakage to connection pool
}
}
5. Isolated Background Processing
using EricksonLopez.MultiTenancy;
using Microsoft.Extensions.DependencyInjection;
public class BackgroundReportWorker
{
private readonly ITenantStore _store;
private readonly ITenantScopeFactory _scopeFactory;
public BackgroundReportWorker(ITenantStore store, ITenantScopeFactory scopeFactory)
{
_store = store;
_scopeFactory = scopeFactory;
}
public async Task ExecuteTenantJobAsync(TenantId tenantId, CancellationToken ct)
{
// GetTenantAsync returns Result<ITenantInfo> โ use IsSuccess to verify
var result = await _store.GetTenantAsync(tenantId, ct);
if (!result.IsSuccess || !result.Value.IsActive) return;
// Creates an isolated DI scope with pre-populated Scoped ITenantContext
await using var scope = _scopeFactory.CreateScope(result.Value);
var reportEngine = scope.ServiceProvider.GetRequiredService<IReportGenerator>();
await reportEngine.GenerateDailyAuditReportAsync(ct);
}
}
๐ก Core Use Cases
1. Clean Architecture & CQRS Query Handlers
In CQRS architectures, handlers enforce tenant boundaries through explicit constructor injection of ITenantContext, avoiding ambient static references.
using System.Data.Common;
using Dapper;
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.Dapper;
using MediatR;
public sealed record GetCustomerByIdQuery(Guid CustomerId) : IRequest<CustomerDto?>;
public sealed class GetCustomerByIdHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto?>
{
private readonly ITenantContext _tenantContext;
private readonly DbConnection _dbConnection;
public GetCustomerByIdHandler(ITenantContext tenantContext, DbConnection dbConnection)
{
_tenantContext = tenantContext;
_dbConnection = dbConnection;
}
public async Task<CustomerDto?> Handle(GetCustomerByIdQuery request, CancellationToken ct)
{
// CreateTenantParameters() returns a DynamicParameters bag pre-populated with @TenantId.
var parameters = _tenantContext.CreateTenantParameters();
parameters.Add("CustomerId", request.CustomerId);
return await _dbConnection.QuerySingleOrDefaultAsync<CustomerDto>(
"SELECT id, name, email FROM customers WHERE id = @CustomerId AND tenant_id = @TenantId",
parameters);
}
}
2. Multi-Strategy Resolution with Fail-Closed Conflict Detection
Configure multi-tier resolution where authenticated tokens always take precedence over subdomains or headers. Conflicting vectors fail immediately (ADR-008).
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.AspNetCore;
builder.Services.AddMultiTenancy();
builder.Services.AddAspNetCoreMultiTenancy();
// 1. Priority 1 (Default): Authenticated JWT Claims ('tenant_id', 'tid')
// 2. Priority 2: Subdomain / HostName Strategy (acme.platform.com -> 'acme')
builder.Services.AddHostNameTenantStrategy();
// 3. Priority 3: Route Value (/api/{tenantId}/orders)
builder.Services.AddRouteTenantStrategy("tenantId");
// 4. Priority 4: Internal Header Strategy (Opt-In for trusted API gateways)
builder.Services.AddInternalHeaderTenantResolution(
expectedSharedSecret: "your-gateway-secret-here",
headerName: "X-Tenant-ID", // optional, default is "X-Tenant-ID"
secretHeaderName: "X-Gateway-Secret" // optional, default is "X-Gateway-Secret"
);
3. Per-Tenant Configuration & Feature Options
Bind configuration options dynamically per tenant without restarting the application.
public sealed class TenantPaymentGatewayOptions
{
public string MerchantId { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public bool EnableCryptoCheckout { get; set; }
}
// In Program.cs:
builder.Services.AddPerTenantOptions<TenantPaymentGatewayOptions, TenantInfo>((options, tenant) =>
{
options.MerchantId = $"MERCHANT_{tenant.Name.ToUpperInvariant()}";
options.EnableCryptoCheckout = tenant.Name == "enterprise-corp";
});
4. Background Job & Message Queue Consumers
Consume messages from RabbitMQ, Azure Service Bus, or Hangfire while guaranteeing strict tenant isolation.
using EricksonLopez.MultiTenancy;
using Microsoft.Extensions.DependencyInjection;
public sealed class OrderPlacedConsumer
{
private readonly ITenantStore _tenantStore;
private readonly ITenantScopeFactory _scopeFactory;
public OrderPlacedConsumer(ITenantStore tenantStore, ITenantScopeFactory scopeFactory)
{
_tenantStore = tenantStore;
_scopeFactory = scopeFactory;
}
public async Task ConsumeAsync(OrderPlacedEvent message, CancellationToken ct)
{
// GetTenantAsync returns Result<ITenantInfo> โ use IsSuccess and .Value
var result = await _tenantStore.GetTenantAsync(message.TenantId, ct);
if (!result.IsSuccess || !result.Value.IsActive)
{
throw new InvalidOperationException($"Invalid or inactive tenant {message.TenantId}");
}
// Creates a dedicated DI container scope with Scoped ITenantContext populated
await using var scope = _scopeFactory.CreateScope(result.Value);
var processor = scope.ServiceProvider.GetRequiredService<IOrderFulfillmentService>();
await processor.FulfillOrderAsync(message.OrderId, ct);
}
}
5. Per-Tenant Authentication Schemes & Dynamic Cookies
Isolate cookie authentication sessions across different tenant subdomains to prevent session cross-contamination.
using EricksonLopez.MultiTenancy.Authentication;
builder.Services.AddPerTenantAuthentication<TenantInfo>();
builder.Services.AddScoped<TenantCookieAuthenticationEvents<TenantInfo>>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "TenantCookieScheme";
})
.AddCookie("TenantCookieScheme", options =>
{
options.EventsType = typeof(TenantCookieAuthenticationEvents<TenantInfo>);
});
6. Database-per-Tenant Dynamic Connection Routing
For hybrid architectures where certain tenants require dedicated physical SQLite databases while others share infrastructure.
using System.Data.Common;
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.Sqlite;
public sealed class TenantDatabaseRouter
{
private readonly ISqliteTenantConnectionFactory _connectionFactory;
private readonly ITenantContext _tenantContext;
public TenantDatabaseRouter(ISqliteTenantConnectionFactory connectionFactory, ITenantContext tenantContext)
{
_connectionFactory = connectionFactory;
_tenantContext = tenantContext;
}
public async Task<DbConnection> GetTenantDatabaseConnectionAsync(CancellationToken ct = default)
{
var tenant = _tenantContext.RequiredTenant;
return await _connectionFactory.CreateConnectionAsync(tenant.Id, ct);
}
}
๐ Configuration & Integrations
ASP.NET Core & Minimal APIs
Integrate seamlessly into ASP.NET Core request pipelines with built-in endpoint security filters.
var app = builder.Build();
// 1. Resolution middleware must run after Authentication to access User Claims
app.UseAuthentication();
app.UseMultiTenancy();
app.UseAuthorization();
// 2. Secure endpoint groups with .RequireTenant()
var tenantGroup = app.MapGroup("/api/v1/workspaces")
.RequireTenant();
tenantGroup.MapGet("/", (ITenantContext context) =>
{
return Results.Ok(new { Tenant = context.RequiredTenant });
});
OpenTelemetry Tracing & Metrics
Propagate tenant context across distributed traces via W3C Baggage and monitor resolution metrics.
using EricksonLopez.MultiTenancy.OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
builder.Services.AddMultiTenancyOpenTelemetry();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(TenantActivitySource.ActivitySourceName)
.AddAspNetCoreInstrumentation())
.WithMetrics(metrics => metrics
.AddMeter(TenantMetrics.MeterName));
Multi-Tenancy Health Checks
Verify store connectivity and tenant resolution health during application startup.
using EricksonLopez.MultiTenancy;
builder.Services.AddMultiTenancyHealthCheck(options =>
{
options.IncludeDiagnosticData = true;
options.StoreProbe = async (store, ct) =>
{
var probeResult = await store.GetTenantAsync(SeedTenants.AcmeId, ct);
return probeResult.IsSuccess;
};
});
builder.Services.AddHealthChecks();
Caching & Tenant Store Strategies
Mitigate database lookups during resolution by wrapping underlying stores with memory caching.
// 1. Register base database store
builder.Services.AddPostgreSqlTenantStore<TenantInfo>(connectionString);
// 2. Wrap with thread-safe IMemoryCache decorator
builder.Services.AddCachedTenantStore<TenantInfo>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
options.SlidingExpiration = TimeSpan.FromMinutes(15);
});
Roslyn Diagnostic Analyzers Reference
EricksonLopez.MultiTenancy.Analyzers evaluates code during compilation:
| Diagnostic ID | Severity | Category | Description | CodeFix Available |
|---|---|---|---|---|
ELMT001 |
Error | Security / Reliability | Prohibits storing ITenantContext or ITenantContextAccessor in static fields. |
โ (Manual Refactor) |
ELMT002 |
Error | Architecture / DI | Prohibits injecting Scoped ITenantContext into Singleton lifetime services. |
โ (Manual Refactor) |
ELMT003 |
Warning | Defense-in-Depth | Warns when Dapper queries are executed without explicit tenant parameter helpers. | โ (Auto Parameterize) |
Organization Hierarchy Interfaces
For enterprise SaaS applications that model multi-dimensional organizational structures, the Abstractions package provides a hierarchy of context interfaces:
using EricksonLopez.MultiTenancy;
// IBranchContext โ operational location / physical branch context
public interface IBranchContext
{
Guid? BranchId { get; }
IReadOnlyList<Guid> AllowedBranchIds { get; }
bool AllBranchesAllowed { get; }
}
// ICompanyContext โ legal entity / company context
public interface ICompanyContext
{
Guid? CompanyId { get; }
bool HasCompanyContext { get; } // true when CompanyId is assigned and non-empty
}
// IOrganizationContext โ combines ITenantContext + ICompanyContext + IBranchContext
public interface IOrganizationContext : ITenantContext, ICompanyContext, IBranchContext { }
Use OrganizationDapperExtensions (from EricksonLopez.MultiTenancy.Dapper) for Dapper query parameterization across all organizational dimensions:
// Add company and branch parameters alongside TenantId
var parameters = new DynamicParameters();
parameters.WithTenant(tenantContext);
parameters.WithCompany(organizationContext);
parameters.WithBranch(organizationContext);
Cross-Tenant Platform Admin Context
IPlatformAdminContext provides an explicit, non-nullable bypass contract for platform-level administrative operations that must span tenant boundaries without manipulating the ambient ITenantContext:
using EricksonLopez.MultiTenancy;
// Inject IPlatformAdminContext โ only valid when IsPlatformAdmin is true
public class PlatformAdminReportingService
{
private readonly IPlatformAdminContext _adminContext;
public PlatformAdminReportingService(IPlatformAdminContext adminContext)
{
_adminContext = adminContext;
}
public async Task GenerateCrossOrganizationReportAsync(CancellationToken ct)
{
if (!_adminContext.IsPlatformAdmin)
throw new UnauthorizedAccessException("Platform admin context required.");
// Safe to query across tenants here
}
}
HTTP Remote Tenant Store
HttpRemoteTenantStore<TTenant> fetches tenant metadata from a remote HTTP catalog service in microservice architectures:
builder.Services.AddHttpRemoteTenantStore<TenantInfo>(options =>
{
options.BaseAddress = new Uri("https://tenant-catalog.internal/");
options.Timeout = TimeSpan.FromSeconds(5);
options.EndpointTemplate = "/api/tenants/{0}"; // optional, this is the default
});
// Typically combined with CachedTenantStore to avoid per-request HTTP calls:
builder.Services.AddCachedTenantStore<TenantInfo>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
});
๐งช Testing & Quality
Testing Primitives & Doubles
The EricksonLopez.MultiTenancy.Testing package provides pre-configured fakes:
FakeTenantStore<TTenant>: In-memory store double for unit testing lookups.FakeTenantResolutionStrategy: Configurable strategy double to simulate header, route, or JWT resolution.TenantContextBuilder: Fluent builder for creating populatedITenantContextinstances without mock libraries.FakeDbInfrastructure: In-memory ADO.NET connection and transaction doubles.
Unit & Integration Test Example
using EricksonLopez.MultiTenancy;
using EricksonLopez.MultiTenancy.Testing;
using Xunit;
public sealed class CustomerServiceTests
{
[Fact]
public async Task GetCustomerAsync_WhenTenantIsActive_ReturnsScopedCustomer()
{
// 1. Arrange: Build test doubles
var tenantId = TenantId.NewId();
var tenantContext = new TenantContextBuilder()
.WithId(tenantId)
.WithName("test-tenant")
.WithIsActive(true)
.Build();
var fakeDb = new FakeDbConnection();
var sut = new CustomerService(tenantContext, fakeDb);
// 2. Act
var result = await sut.GetCustomerAsync(Guid.NewGuid());
// 3. Assert
Assert.NotNull(result);
Assert.Equal(tenantId, sut.CurrentTenantId);
}
}
Architectural Boundary Verification
The solution includes an automated architectural test suite (EricksonLopez.MultiTenancy.ArchitectureTests) powered by ArchUnitNET and NetArchTest.Rules:
- Verifies that
Abstractionsdoes not reference database drivers (Npgsql,Microsoft.Data.SqlClient). - Ensures
ITenantContextAccessoris never registered as a Singleton. - Guarantees Native AOT rules (no unannotated reflection in pipeline handlers).
Quality Gate & Mutation Testing Metrics
| Metric | Target | Verified Status | Quality Gate |
|---|---|---|---|
| Line Coverage | 100.00% | 100.00% (15/15 packages) | Required on all PRs |
| Branch Coverage | 100.00% | 100.00% (15/15 packages) | Required on all PRs |
| Method Coverage | 100.00% | 100.00% (15/15 packages) | Required on all PRs |
| Stryker Mutation Score | โฅ 95.00% | 100.00% (High Threshold) | Verified in publish.yml |
| Native AOT Smoke Test | 100% Pass | 100% Pass (AotSmokeTest) |
Required in CI |
โก Performance Benchmarks
Environment: .NET 10.0.10, X64 RyuJIT AVX-512, BenchmarkDotNet v0.15.8
Primary Operations Benchmark
Comparison of EricksonLopez.MultiTenancy against conventional ambient dictionary and reflection-based multitenancy models:
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|---|
TenantId.Create(Guid) |
1.85 ns | 0.04 ns | 0.03 ns | 1.00 | - | 0 B |
TenantContextAccessor.GetContext() |
1.12 ns | 0.02 ns | 0.01 ns | 0.61 | - | 0 B |
TenantId.StructEquality |
0.42 ns | 0.01 ns | 0.01 ns | 0.23 | - | 0 B |
Conventional_AmbientAsyncLocal_DictionaryLookup |
18.94 ns | 0.35 ns | 0.28 ns | 10.24 | 0.0038 | 24 B |
Conventional_ConcurrentDictionary_CacheLookup |
24.15 ns | 0.42 ns | 0.38 ns | 13.05 | 0.0076 | 48 B |
Conventional_Regex_SubdomainExtraction |
142.80 ns | 2.10 ns | 1.85 ns | 77.19 | 0.0229 | 144 B |
Conventional_String_Boxing_And_Concatenation |
88.50 ns | 1.25 ns | 1.10 ns | 47.84 | 0.0305 | 192 B |
Multi-Strategy Resolution & Conflict Detection
Execution timings for single vs multi-strategy resolution chains:
| Method | Mean | Ratio | Gen0 | Allocated |
|---|---|---|---|---|
SingleStrategy_Header_Resolve (Baseline) |
32.4 ns | 1.00 | - | 0 B |
SingleStrategy_Claim_Resolve |
28.1 ns | 0.87 | - | 0 B |
SingleStrategy_BasePath_Resolve |
21.6 ns | 0.67 | - | 0 B |
MultiStrategy_Sequential_Fallback |
34.2 ns | 1.05 | - | 0 B |
MultiStrategy_FailClosed_ConflictDetection |
48.7 ns | 1.50 | - | 0 B |
Relational Database Session & Parameter Injection
Overhead of setting up Row Level Security and Dapper parameters:
| Method | Mean | Ratio | Gen0 | Allocated |
|---|---|---|---|---|
Dapper_CreateTenantParameters (Baseline) |
42.1 ns | 1.00 | 0.0178 | 112 B |
Dapper_WithTenant_Extension |
46.5 ns | 1.10 | 0.0178 | 112 B |
PostgreSql_SetLocal_RlsContext |
68.4 ns | 1.62 | 0.0204 | 128 B |
SqlServer_SetSessionContext |
64.2 ns | 1.52 | 0.0204 | 128 B |
๐ Compatibility & Technical Matrix
Framework & Compilation Target Matrix
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0ยน | NativeAOT | Trimmable | SNK Signed |
|---|---|---|---|---|---|---|
EricksonLopez.MultiTenancy.Abstractions |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy (Core) |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Analyzers |
netstandard2.0 |
netstandard2.0 |
netstandard2.0 |
N/A | N/A | โ |
EricksonLopez.MultiTenancy.AspNetCore |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Authentication |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Configuration |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Dapper |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.PostgreSql |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.SqlServer |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.MySql |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.MariaDb |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Oracle |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Sqlite |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.OpenTelemetry |
โ | โ | โก | โ | โ | โ |
EricksonLopez.MultiTenancy.Testing |
โ | โ | โก | โ | โ | โ |
ยน โก Forward-compatible via
net9.0binary. Packages targetnet8.0;net9.0explicitly. .NET 10.0 projects consume thenet9.0TFM through forward-compatibility. Full backward compatibility is guaranteed until Microsoft officially reaches End-of-Life (EOL) for .NET 8 and .NET 9 in November 2026, at which milestone the ecosystem will transition to .NET 10 and .NET 11 as first-class TFM targets.
Relational Database Dialect Isolation Matrix
| Database Dialect | Isolation Mechanism | Transaction Scoping | Pooled Connection Safe |
|---|---|---|---|
| PostgreSQL | SET LOCAL app.current_tenant_id = :tenantId + RLS |
โ Yes | โ 100% Safe (Auto Cleared) |
| Microsoft SQL Server | sp_set_session_context 'tenant_id', @TenantId |
โ Yes | โ 100% Safe (Reset on Rollback) |
| MySQL | @app_tenant_id session variable binding |
โ Yes | โ 100% Safe |
| MariaDB | @app_tenant_id session variable binding |
โ Yes | โ 100% Safe |
| Oracle Database | DBMS_SESSION.SET_IDENTIFIER + VPD |
โ Yes | โ 100% Safe |
| SQLite | Database-per-tenant (ISqliteTenantConnectionFactory) |
โ Yes | โ 100% Safe |
HTTP Status Code & Security Error Mapping
| Scenario | HTTP Status Code | RFC 9457 Problem Details Type | Action |
|---|---|---|---|
| Strategy Conflict Detected (ADR-008) | 409 Conflict |
https://httpstatuses.com/409#tenant-conflict |
Abort request immediately (throws TenantResolutionConflictException or writes Problem Details) |
| Missing Required Tenant | 401 Unauthorized |
https://httpstatuses.com/401#missing-tenant |
Challenge authentication |
| Tenant Inactive / Disabled | 403 Forbidden |
https://httpstatuses.com/403#tenant-inactive |
Reject client access |
| Tenant Not Found in Store | 404 Not Found |
https://httpstatuses.com/404#tenant-not-found |
Terminate routing |
๐๏ธ Architecture & Design Principles
4-Layer Defense-in-Depth Model
flowchart TD
subgraph L1["Layer 1: Application Layer"]
L1_App["Explicit SQL Parameters\nWHERE tenant_id = @TenantId"]
end
subgraph L2["Layer 2: Infrastructure Layer"]
L2_Accessor["Scoped Write-Once Accessor\nITenantContext (No Static Leaks)"]
L2_Dapper["Dapper Extension Helpers\nWithTenant(parameters)"]
end
subgraph L3["Layer 3: Transaction-Scoped Context"]
L3_Txn["Atomic SET LOCAL app.current_tenant_id\nWithin Database Transaction"]
end
subgraph L4["Layer 4: Database Engine RLS"]
L4_RLS["PostgreSQL FORCE ROW LEVEL SECURITY\nRESTRICTIVE USING & WITH CHECK"]
end
L1 --> L2
L2 --> L3
L3 --> L4
Request Resolution Lifecycle Sequence
sequenceDiagram
autonumber
actor Client as HTTP Client
participant MW as TenantResolutionMiddleware
participant Strat as Resolution Strategies
participant Store as ITenantStore
participant Acc as ScopedTenantContextAccessor
participant Endpoint as Minimal API / Controller
Client->>MW: HTTP Request
MW->>Strat: Execute Strategies (Claim > Host > Route > Header)
Strat-->>MW: Candidate TenantId
alt Strategy Conflict Detected
MW-->>Client: HTTP 409 Conflict (ADR-008 Conflict) or throws TenantResolutionConflictException
else Resolved TenantId
MW->>Store: GetTenantAsync(tenantId)
Store-->>MW: ITenantInfo (IsActive check)
alt Inactive / Not Found
MW-->>Client: HTTP 401 Unauthorized / 404 Not Found
else Active Tenant
MW->>Acc: Set TenantContext (Write-Once)
MW->>Endpoint: Next(HttpContext)
Endpoint-->>Client: HTTP 200 OK Response
end
end
Tenant Context Lifecycle State Machine
stateDiagram-v8
[*] --> Unresolved : Request Initiated
Unresolved --> Resolving : TenantResolutionMiddleware
Resolving --> ConflictDetected : Conflicting Strategies
ConflictDetected --> Terminated : HTTP 409 Conflict
Resolving --> Resolved : Matching Candidate
Resolved --> StoreLookup : ITenantStore.GetTenantAsync
StoreLookup --> NotFound : Unknown TenantId
NotFound --> Terminated : HTTP 404 Not Found
StoreLookup --> Inactive : IsActive == false
Inactive --> Terminated : HTTP 403 Forbidden
StoreLookup --> Active : IsActive == true
Active --> Initialized : Write ScopedTenantContextAccessor
Initialized --> ExecutingPipeline : Endpoint Execution
ExecutingPipeline --> [*] : Scope Disposal & Reset
Package Layering & Dependency Hierarchy
graph TD
Abstractions["EricksonLopez.MultiTenancy.Abstractions\n(L0: Pure Contracts & TenantId)"]
Core["EricksonLopez.MultiTenancy\n(L1: Engine & Scope Factory)"]
Analyzers["EricksonLopez.MultiTenancy.Analyzers\n(L2: Roslyn Rules)"]
AspNetCore["EricksonLopez.MultiTenancy.AspNetCore\n(L3: Middleware & Strategies)"]
Authentication["EricksonLopez.MultiTenancy.Authentication\n(L3: Per-Tenant Auth)"]
Configuration["EricksonLopez.MultiTenancy.Configuration\n(L3: IConfiguration Store)"]
Dapper["EricksonLopez.MultiTenancy.Dapper\n(L4: Parameters)"]
OpenTelemetry["EricksonLopez.MultiTenancy.OpenTelemetry\n(L6: Tracing & Metrics)"]
Testing["EricksonLopez.MultiTenancy.Testing\n(L7: Test Harness)"]
PostgreSql["EricksonLopez.MultiTenancy.PostgreSql\n(L5: PostgreSQL RLS)"]
SqlServer["EricksonLopez.MultiTenancy.SqlServer\n(L5: SESSION_CONTEXT)"]
MySql["EricksonLopez.MultiTenancy.MySql\n(L5: MySQL Session)"]
MariaDb["EricksonLopez.MultiTenancy.MariaDb\n(L5: MariaDB Session)"]
Oracle["EricksonLopez.MultiTenancy.Oracle\n(L5: Oracle VPD)"]
Sqlite["EricksonLopez.MultiTenancy.Sqlite\n(L5: DB-Per-Tenant)"]
Core --> Abstractions
Analyzers --> Abstractions
AspNetCore --> Abstractions
Authentication --> AspNetCore
Configuration --> Core
Configuration --> Abstractions
Dapper --> Abstractions
OpenTelemetry --> Abstractions
Testing --> Core
Testing --> Abstractions
PostgreSql --> Abstractions
PostgreSql --> Dapper
SqlServer --> Abstractions
SqlServer --> Dapper
MySql --> Abstractions
MySql --> Dapper
MariaDb --> Abstractions
MariaDb --> Dapper
Oracle --> Abstractions
Oracle --> Dapper
Sqlite --> Abstractions
Sqlite --> Dapper
๐ก๏ธ Best Practices & Anti-Patterns
| Scenario | โ Avoid | โ Recommended |
|---|---|---|
| Context Storage | Storing ITenantContext in static fields (ELMT001). |
Injecting ITenantContext into Scoped constructors. |
| Service Lifetime | Injecting ITenantContext into Singleton services (ELMT002). |
Registering services as Scoped or using ITenantScopeFactory. |
| SQL Queries | Omitting @TenantId parameter in Dapper queries (ELMT003). |
Using _tenantContext.CreateTenantParameters(...). |
| Database Isolation | Using session-level SET app.tenant_id = ... (Leaks in pool). |
Using transaction-scoped SET LOCAL inside transactions. |
| Resolution Precedence | Allowing HTTP headers (X-Tenant-ID) to override JWT claims. |
Enforcing claims-first priority and failing closed on conflicts (ADR-008). |
| Background Processing | Sharing ambient AsyncLocal state across background threads. |
Creating isolated scopes via ITenantScopeFactory.CreateScope(tenant). |
| Identifier Types | Using un-typed string tenantId or Guid? nullable values. |
Using immutable TenantId readonly record struct. |
| Query Rewriting | Dynamic regex or AST SQL string rewriting in request hot paths. | Writing explicit SQL with PostgreSQL RLS as database-tier enforcement. |
โ ๏ธ Troubleshooting & Common Pitfalls
Always ensure database connections connect using an unprivileged application role (e.g., app_user). Connecting as PostgreSQL postgres superuser bypasses RLS policies unless FORCE ROW LEVEL SECURITY is applied.
1. TenantResolutionConflictException: Resolution conflict detected between strategies
- Symptom: API returns
HTTP 409 Conflict(whenWriteProblemDetailsOnConflict = true) or throwsTenantResolutionConflictException(whenfalse) with an ADR-008 conflict message. - Root Cause: Two configured strategies resolved conflicting tenant identifiers on the same request (e.g., JWT Claim was Tenant A, but
X-Tenant-IDheader was Tenant B). - Remediation: Remove contradictory client headers. Claims always take precedence in authenticated contexts.
2. InvalidOperationException: ITenantContextAccessor has already been set
- Symptom: Exception thrown when setting
tenantContextAccessor.TenantContext = .... - Root Cause: Attempting to mutate tenant context within an active request scope.
ScopedTenantContextAccessoris write-once per scope. - Remediation: Never overwrite active context. For background tasks targeting different tenants, create a new DI scope via
ITenantScopeFactory.CreateScope(targetTenant).
3. PostgreSQL Query Returns 0 Rows Unexpectedly
- Symptom: Queries execute without throwing errors but return empty datasets.
- Root Cause: PostgreSQL RLS is enabled on the table, but
SET LOCAL app.current_tenant_idwas not executed, causingcurrent_setting('app.current_tenant_id', true)to returnNULL. - Remediation: Execute database operations within
connection.BeginTenantTransactionAsync(tenantContext)referencing the active transaction wrapper.
4. Roslyn Diagnostic ELMT002: Cannot inject Scoped ITenantContext into Singleton
- Symptom: Build fails with
ELMT002error. - Root Cause: A singleton service captures a scoped tenant context, creating a captive dependency.
- Remediation: Change the service lifetime to
Scopedor injectIServiceProvider/ITenantScopeFactoryto resolve the context on-demand.
๐ Part of the Ecosystem
EricksonLopez.MultiTenancy is an integral component of the Erickson Lopez enterprise .NET architectural ecosystem:
- ๐งฑ EricksonLopez.SharedKernel โ Foundational Domain Primitives, Specifications, and Domain Events for Clean Architecture.
- โก EricksonLopez.Result โ High-Performance Struct-Based Result Pattern and Railway-Oriented Programming.
- ๐ EricksonLopez.Specification โ Composable, Native AOT-First Specification Pattern for Query & Validation.
- ๐ EricksonLopez.Mediator โ Zero-Allocation Struct-Based In-Memory CQRS Mediator.
๐ค Contributing
We welcome community contributions. Please adhere to the following workflow for local development:
1. Prerequisites
- .NET 10.0 SDK, .NET 9.0 SDK, or .NET 8.0 SDK.
- Git.
- Stryker.NET (
dotnet tool install --global dotnet-stryker).
2. Build & Verify Locally
# 1. Clone the repository
git clone https://github.com/ericksonlopezf/dotnet-multitenancy.git
cd dotnet-multitenancy
# 2. Build solution in Release configuration
dotnet build EricksonLopez.MultiTenancy.slnx --configuration Release
# 3. Run complete unit, integration, and architecture test suites
dotnet test EricksonLopez.MultiTenancy.slnx --configuration Release
# 4. Run mutation testing gate
dotnet stryker --config-file stryker-config.json
Please review the Contributing Guidelines and Code of Conduct prior to submitting Pull Requests.
๐ License
Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.
| 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 is compatible. 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
- EricksonLopez.MultiTenancy.Abstractions (>= 2.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- OpenTelemetry.Api (>= 1.19.1)
-
net9.0
- EricksonLopez.MultiTenancy.Abstractions (>= 2.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- OpenTelemetry.Api (>= 1.19.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.