EricksonLopez.MultiTenancy.Oracle 1.0.0

dotnet add package EricksonLopez.MultiTenancy.Oracle --version 1.0.0
                    
NuGet\Install-Package EricksonLopez.MultiTenancy.Oracle -Version 1.0.0
                    
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="EricksonLopez.MultiTenancy.Oracle" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.MultiTenancy.Oracle" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.MultiTenancy.Oracle" />
                    
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 EricksonLopez.MultiTenancy.Oracle --version 1.0.0
                    
#r "nuget: EricksonLopez.MultiTenancy.Oracle, 1.0.0"
                    
#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 EricksonLopez.MultiTenancy.Oracle@1.0.0
                    
#: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=EricksonLopez.MultiTenancy.Oracle&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.MultiTenancy.Oracle&version=1.0.0
                    
Install as a Cake Tool

EricksonLopez.MultiTenancy

Secure, high-performance, Native AOT-compatible, enterprise-grade multi-tenancy ecosystem and 4-layer Defense-in-Depth isolation for modern .NET.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT


Executive Summary

EricksonLopez.MultiTenancy is a foundational, Native AOT-first multi-tenancy ecosystem engineered for .NET 8.0, .NET 9.0, and .NET 10.0 applications built with Clean Architecture, Domain-Driven Design (DDD), and relational persistence engines (PostgreSQL, 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

Traditional Multi-Tenancy Anti-Patterns

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 via SET LOCAL (PostgreSQL) or sp_set_session_context (SQL Server). When a transaction completes (COMMIT or ROLLBACK), the database automatically purges the variable, returning clean connections to the pool.
  • ๐Ÿงฑ Immutable, Zero-Allocation Struct TenantId: A 128-bit Guid-backed readonly record struct implementing IEquatable<TenantId> and IComparable<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 with HTTP 400 Bad Request.
  • ๐Ÿ•ต๏ธ Compile-Time Roslyn Analyzers: Analyzers ELMT001, ELMT002, and ELMT003 intercept 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 TenantId Primitives: 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: ITenantScopeFactory creates 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, and TenantContextBuilder for frictionless unit and integration testing.

๐Ÿ“ฆ Ecosystem

Package Version Description
EricksonLopez.MultiTenancy.Abstractions NuGet Foundational contracts: TenantId, ITenantInfo, ITenantContext, ITenantResolver, ITenantScope, and TenantErrors.
EricksonLopez.MultiTenancy NuGet Core engine: ScopedTenantContextAccessor, DefaultTenantScopeFactory, InMemoryTenantStore, and CachedTenantStore.
EricksonLopez.MultiTenancy.Analyzers NuGet Roslyn analyzers: static field leaks (ELMT001), singleton captivity (ELMT002), un-scoped Dapper queries (ELMT003).
EricksonLopez.MultiTenancy.AspNetCore NuGet ASP.NET Core resolution middleware, JWT/Host/Route/Header strategies, .RequireTenant() endpoint filters.
EricksonLopez.MultiTenancy.Authentication NuGet Per-tenant authentication schemes, cookie validation events, dynamic scheme routing.
EricksonLopez.MultiTenancy.Configuration NuGet IConfiguration and IOptionsMonitor-backed tenant store for file-based configuration.
EricksonLopez.MultiTenancy.Dapper NuGet Dapper parameter builders and query parameter helpers (WithTenant, CreateTenantParameters).
EricksonLopez.MultiTenancy.PostgreSql NuGet PostgreSQL Row Level Security (RLS) enforcement via transaction-scoped SET LOCAL + PostgreSqlTenantStore.
EricksonLopez.MultiTenancy.SqlServer NuGet SQL Server SESSION_CONTEXT management via sp_set_session_context and security policies.
EricksonLopez.MultiTenancy.MySql NuGet MySQL session variable isolation (@app_tenant_id) and connection scoping.
EricksonLopez.MultiTenancy.MariaDb NuGet MariaDB session variable isolation (@app_tenant_id) and connection scoping.
EricksonLopez.MultiTenancy.Oracle NuGet Oracle Virtual Private Database (VPD) and DBMS_SESSION.SET_IDENTIFIER integration.
EricksonLopez.MultiTenancy.Sqlite NuGet SQLite database-per-tenant (ISqliteTenantConnectionFactory) and temp table session context.
EricksonLopez.MultiTenancy.OpenTelemetry NuGet Distributed tracing (TenantActivitySource), W3C Baggage propagation, metrics (TenantMetrics).
EricksonLopez.MultiTenancy.Testing NuGet 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 10)

The repository provides a runnable reference implementation showcasing all 11 progressive curriculum levels:

dotnet run --project samples/EricksonLopez.MultiTenancy.Showcase
Level Topic Description
Level 00 Domain Primitives & Invariants TenantId struct, ITenantInfo, immutability guarantees, and TenantErrors.
Level 01 Quick Start & Core Pipeline Minimal DI setup, resolution middleware, and .RequireTenant() endpoint filters.
Level 02 Multi-Strategy & Options Host, Route, BasePath, Claims strategies, per-tenant options, and cached stores.
Level 03 Real-World Data Access Explicit Dapper repositories, query parameterization, and cookie authentication events.
Level 04 Database Dialects & RLS PostgreSQL SET LOCAL, SQL Server SESSION_CONTEXT, MySQL, MariaDB, Oracle, SQLite.
Level 05 Background Scopes Non-HTTP message consumers using ITenantScopeFactory without ambient leakage.
Level 06 Fail-Closed Conflict Guards Resolution conflict detection (ADR-008), inactive tenant guards, and Problem Details.
Level 07 Scalability & Database Routing SQLite DB-per-tenant dynamic connection factories and multi-tier memory caching.
Level 08 Custom Tenant Models Strongly-typed custom tenant metadata and domain-specific resolution strategies.
Level 09 Telemetry & Observability OpenTelemetry distributed tracing, W3C Baggage propagation, metrics, and health checks.
Level 10 Enterprise Defense-in-Depth Full 4-layer integration, architectural testing with ArchUnitNET, and mock harnesses.

๐Ÿ“– Technical Reference & Architecture Guides


๐Ÿ“ฅ 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 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 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)
    {
        var tenant = await _store.GetTenantAsync(tenantId, ct);
        if (tenant is null || !tenant.IsActive) return;

        // Creates an isolated DI scope with pre-populated Scoped ITenantContext
        await using var scope = _scopeFactory.CreateScope(tenant);

        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.

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)
    {
        // Explicitly injects @TenantId parameter into the Dapper parameter bag
        var parameters = _tenantContext.CreateTenantParameters(new { 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).

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("X-Tenant-ID");

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.

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)
    {
        var tenant = await _tenantStore.GetTenantAsync(message.TenantId, ct);
        if (tenant is null || !tenant.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(tenant);
        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.

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 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.HealthChecks;

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)

๐Ÿงช 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 populated ITenantContext instances 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 Abstractions does not reference database drivers (Npgsql, Microsoft.Data.SqlClient).
  • Ensures ITenantContextAccessor is 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 Identity & Resolution Benchmarks

Method Mean Error StdDev Allocated
TenantId.Create(Guid) 0.0000 ns 0.0000 ns 0.0000 ns 0 B
TenantId.TryCreate(ReadOnlySpan<char>) 3.1245 ns 0.0210 ns 0.0196 ns 0 B
TenantId.ToString(SpanFormat) 5.4120 ns 0.0340 ns 0.0318 ns 0 B
ScopedTenantContextAccessor.GetTenantContext() 0.0000 ns 0.0000 ns 0.0000 ns 0 B
Dapper.CreateTenantParameters(state) 12.3840 ns 0.0820 ns 0.0767 ns 0 B
CachedTenantStore.GetTenantAsync(Hit) 18.6210 ns 0.1140 ns 0.1066 ns 0 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 โœ… โœ… โœ… โœ… โœ… โœ…

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) 400 Bad Request https://httpstatuses.com/400#tenant-conflict Abort request immediately
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 400 Bad Request (ADR-008 Conflict)
    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 400 Bad Request
    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. InvalidOperationException: Resolution conflict detected between strategies

  • Symptom: API returns HTTP 400 Bad Request 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-ID header 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. ScopedTenantContextAccessor is 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_id was not executed, causing current_setting('app.current_tenant_id', true) to return NULL.
  • 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 ELMT002 error.
  • Root Cause: A singleton service captures a scoped tenant context, creating a captive dependency.
  • Remediation: Change the service lifetime to Scoped or inject IServiceProvider / ITenantScopeFactory to resolve the context on-demand.

๐ŸŒ Part of the Ecosystem

EricksonLopez.MultiTenancy is an integral component of the Erickson Lopez enterprise .NET architectural ecosystem:


๐Ÿค Contributing

We welcome community contributions. Please adhere to the following workflow for local development:

1. Prerequisites

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

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 80 8/27/2026