CleanArchitecture.Extensions.Multitenancy 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package CleanArchitecture.Extensions.Multitenancy --version 0.2.0
                    
NuGet\Install-Package CleanArchitecture.Extensions.Multitenancy -Version 0.2.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="CleanArchitecture.Extensions.Multitenancy" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CleanArchitecture.Extensions.Multitenancy" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" />
                    
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 CleanArchitecture.Extensions.Multitenancy --version 0.2.0
                    
#r "nuget: CleanArchitecture.Extensions.Multitenancy, 0.2.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 CleanArchitecture.Extensions.Multitenancy@0.2.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=CleanArchitecture.Extensions.Multitenancy&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=CleanArchitecture.Extensions.Multitenancy&version=0.2.0
                    
Install as a Cake Tool

CleanArchitecture.Extensions.Multitenancy

Core multitenancy primitives and MediatR behaviors for Jason Taylor's Clean Architecture template. This package is host-agnostic: it does not depend on ASP.NET Core or EF Core. You supply the host adapter that builds a TenantResolutionContext and sets the current tenant.

What you get

  • Tenant model (TenantInfo), context (TenantContext), and resolution metadata.
  • Resolution pipeline with built-in providers (route/host/header/query/claim/default) and customizable ordering.
  • Validation hooks (ITenantInfoStore, ITenantInfoCache) with cache-or-repository validation.
  • MediatR behaviors for validation, enforcement, correlation, and cache scope alignment.
  • AsyncLocal current-tenant accessor and JSON serializer for background jobs and messages.

Install

dotnet add src/Application/Application.csproj package CleanArchitecture.Extensions.Multitenancy
dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Multitenancy

Optional caching integration (requires CleanArchitecture.Extensions.Caching):

dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Caching

Quickstart

1) Register multitenancy services

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Configuration;

builder.Services.AddCleanArchitectureMultitenancy(options =>
{
    options.HeaderNames = new[] { "X-Tenant-ID" };
    options.RouteParameterName = "tenantId";
    options.QueryParameterName = "tenantId";
    options.ClaimType = "tenant_id";

    // Optional defaults
    // options.FallbackTenantId = "local";
    // options.ValidationMode = TenantValidationMode.Repository;
});

2) Resolve a tenant in your host and set the current context

The core package is host-agnostic. Populate a TenantResolutionContext from your environment and call ITenantResolver. Example: minimal ASP.NET Core middleware:

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;

public sealed class TenantResolutionMiddleware
{
    private readonly RequestDelegate _next;

    public TenantResolutionMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(
        HttpContext httpContext,
        ITenantResolver tenantResolver,
        ITenantAccessor tenantAccessor)
    {
        var resolution = new TenantResolutionContext
        {
            Host = httpContext.Request.Host.Host,
            CorrelationId = httpContext.TraceIdentifier
        };

        foreach (var header in httpContext.Request.Headers)
        {
            resolution.Headers[header.Key] = header.Value.ToString();
        }

        foreach (var route in httpContext.Request.RouteValues)
        {
            if (route.Value is not null)
            {
                resolution.RouteValues[route.Key] = route.Value.ToString()!;
            }
        }

        foreach (var query in httpContext.Request.Query)
        {
            resolution.Query[query.Key] = query.Value.ToString();
        }

        if (httpContext.User?.Identity?.IsAuthenticated == true)
        {
            foreach (var claim in httpContext.User.Claims)
            {
                resolution.Claims[claim.Type] = claim.Value;
            }
        }

        var tenantContext = await tenantResolver.ResolveAsync(resolution, httpContext.RequestAborted);

        using (tenantAccessor.BeginScope(tenantContext))
        {
            await _next(httpContext);
        }
    }
}

3) Add multitenancy pipeline behaviors

using CleanArchitecture.Extensions.Multitenancy.Behaviors;
using MediatR;

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();

    // Existing behaviors...
    // cfg.AddOpenBehavior(typeof(AuthorizationBehaviour<,>));
    // cfg.AddOpenBehavior(typeof(ValidationBehaviour<,>));

    cfg.AddCleanArchitectureMultitenancyPipeline();

    // Optional: warn when cache scope is missing tenant context
    cfg.AddOpenBehavior(typeof(TenantScopedCacheBehavior<,>));
});

4) Use tenant context in handlers or jobs

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;

public sealed class GetTenantSummaryHandler
{
    private readonly ICurrentTenant _currentTenant;

    public GetTenantSummaryHandler(ICurrentTenant currentTenant)
    {
        _currentTenant = currentTenant;
    }

    public string Handle()
    {
        return _currentTenant.TenantId ?? "no-tenant";
    }
}

Configuration highlights

MultitenancyOptions controls resolution order, defaults, and validation behavior:

builder.Services.Configure<MultitenancyOptions>(options =>
{
    options.RequireTenantByDefault = true;
    options.AllowAnonymous = false;
    options.ResolutionOrder = new List<TenantResolutionSource>
    {
        TenantResolutionSource.Route,
        TenantResolutionSource.Host,
        TenantResolutionSource.Header,
        TenantResolutionSource.QueryString,
        TenantResolutionSource.Claim,
        TenantResolutionSource.Default
    };

    options.RequireMatchAcrossSources = false;
    options.IncludeUnorderedProviders = true;
    options.ResolutionCacheTtl = TimeSpan.FromMinutes(5);
    options.AddTenantToLogScope = true;
    options.AddTenantToActivity = true;
});

Key defaults (all configurable):

  • Header name: X-Tenant-ID
  • Route/query parameter: tenantId
  • Claim type: tenant_id
  • Resolution order: Route > Host > Header > Query > Claim > Default
  • Validation mode: None

Providers and resolution order

Built-in providers:

  • RouteTenantProvider, HostTenantProvider, HeaderTenantProvider, QueryTenantProvider, ClaimTenantProvider, DefaultTenantProvider.

Notes:

  • Header/query/claim values can contain multiple candidates separated by , or ;. Multiple candidates are treated as ambiguous and will not resolve a tenant.
  • HostTenantProvider defaults to the first subdomain (for example, tenant.app.comtenant); set HostTenantSelector to customize.
  • RequireMatchAcrossSources collects candidates from all providers and resolves only if there is exactly one unique candidate.

Custom provider example:

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;
using CleanArchitecture.Extensions.Multitenancy.Providers;

builder.Services.AddSingleton<ITenantProvider>(
    new DelegateTenantProvider(context =>
    {
        if (context.Items.TryGetValue("tenant", out var value))
        {
            return value?.ToString();
        }

        return null;
    }));

Validation and lifecycle enforcement

Validation is optional but recommended to prevent spoofed tenant IDs.

builder.Services.Configure<MultitenancyOptions>(options =>
{
    options.ValidationMode = TenantValidationMode.Repository;
    options.ResolutionCacheTtl = TimeSpan.FromMinutes(10);
});

Implement ITenantInfoStore (and optionally ITenantInfoCache):

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;

public sealed class InMemoryTenantStore : ITenantInfoStore
{
    private readonly Dictionary<string, ITenantInfo> _tenants =
        new(StringComparer.OrdinalIgnoreCase)
        {
            ["tenant-1"] = new TenantInfo("tenant-1") { Name = "Tenant One", IsActive = true }
        };

    public Task<ITenantInfo?> FindByIdAsync(string tenantId, CancellationToken cancellationToken = default)
        => Task.FromResult(_tenants.TryGetValue(tenantId, out var tenant) ? tenant : null);
}

TenantEnforcementBehavior throws when:

  • No tenant is resolved (TenantNotResolvedException).
  • Tenant is not validated (TenantNotFoundException).
  • Tenant is suspended, inactive, soft-deleted, pending provision, or expired.

Use AllowHostRequestsAttribute or ITenantRequirement to mark optional endpoints/requests.

Caching integration

When using CleanArchitecture.Extensions.Caching, replace the cache scope so cache keys include tenantId:

using CleanArchitecture.Extensions.Caching;
using CleanArchitecture.Extensions.Multitenancy;

builder.Services.AddCleanArchitectureCaching();
builder.Services.AddCleanArchitectureMultitenancyCaching();

Add the cache scope warning behavior if desired:

cfg.AddOpenBehavior(typeof(TenantScopedCacheBehavior<,>));

Background jobs and messaging

Use ITenantContextSerializer to flow tenant context through job payloads or message headers:

using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Abstractions;

public sealed class TenantJob
{
    private readonly ITenantAccessor _tenantAccessor;
    private readonly ITenantContextSerializer _serializer;

    public TenantJob(ITenantAccessor tenantAccessor, ITenantContextSerializer serializer)
    {
        _tenantAccessor = tenantAccessor;
        _serializer = serializer;
    }

    public Task EnqueueAsync(TenantContext context)
    {
        var payload = _serializer.Serialize(context);
        // store payload in job metadata
        return Task.CompletedTask;
    }

    public Task ExecuteAsync(string payload)
    {
        var context = _serializer.Deserialize(payload);
        using var scope = _tenantAccessor.BeginScope(context);
        // work within tenant boundary
        return Task.CompletedTask;
    }
}

Exceptions

  • TenantNotResolvedException - no tenant resolved when required.
  • TenantNotFoundException - tenant ID could not be validated.
  • TenantInactiveException - tenant is inactive, expired, deleted, or soft-deleted.
  • TenantSuspendedException - tenant is suspended.
  • CleanArchitecture.Extensions.Multitenancy.AspNetCore
  • CleanArchitecture.Extensions.Multitenancy.EFCore
  • CleanArchitecture.Extensions.Multitenancy.Identity
  • CleanArchitecture.Extensions.Multitenancy.Provisioning
  • CleanArchitecture.Extensions.Multitenancy.Redis
  • CleanArchitecture.Extensions.Multitenancy.Sharding
  • CleanArchitecture.Extensions.Multitenancy.Storage

More documentation

See the deep-dive docs under CleanArchitecture.Extensions/docs/extensions/multitenancy-core.md.

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

NuGet packages (3)

Showing the top 3 NuGet packages that depend on CleanArchitecture.Extensions.Multitenancy:

Package Downloads
CleanArchitecture.Extensions.Multitenancy.AspNetCore

ASP.NET Core adapters for Clean Architecture multitenancy middleware, endpoint enforcement, and HTTP resolution.

CleanArchitecture.Extensions.Multitenancy.EFCore

EF Core adapters for Clean Architecture multitenancy, including tenant-aware filters, interceptors, and DbContext helpers.

CleanArchitecture.Extensions.Multitenancy.Caching

Tenant-aware caching integration for Clean Architecture multitenancy, including cache scope binding and cache-scope warnings.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.9 117 1/13/2026
0.2.8 120 1/13/2026
0.2.7 109 1/13/2026
0.2.6 110 1/12/2026
0.2.5 124 1/8/2026
0.2.4 115 1/3/2026
0.2.3 109 1/1/2026
0.2.2 113 1/1/2026
0.2.1 102 12/29/2025
0.2.0 92 12/29/2025
0.1.8-preview.3 51 12/28/2025