Clywell.Core.Tenancy 1.0.0

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

Clywell.Core.Tenancy

NuGet NuGet Downloads Build Status License: MIT

Multi-tenancy plumbing for .NET — tenant context resolution, ASP.NET Core middleware, and Serilog log enrichers. Zero EF Core dependency; pure infrastructure.

Features

  • ITenantContext — scoped service carrying TenantId, TenantName, IsResolved, and the full TenantInfo object for the current request
  • ITenantResolver — pluggable abstraction for tenant resolution; implement it to resolve from headers, subdomains, databases, API keys, or any custom source
  • TenantInfo — extensible record carrying the result of a successful resolution; subclass it to carry additional tenant metadata
  • ClaimsTenantResolver — built-in resolver that reads tid / tenantid JWT claims; used by default when no custom resolver is configured
  • TenantResolutionMiddleware — opt-in middleware that delegates to ITenantResolver and populates ITenantContext per request
  • TenancyOptions — configuration object accepted by AddTenancy() for declaring a custom resolver at registration time
  • TenantLogEnricher — Serilog enricher that appends TenantId and TenantName to every log event
  • UserLogEnricher — Serilog enricher that appends UserId from the authenticated user's claims
  • AddTenancy() — single DI registration call; no-arg form uses the default claims resolver

Installation

dotnet add package Clywell.Core.Tenancy

Quick Start

1. Register services

// Program.cs — default: resolves tenant from JWT claims
builder.Services.AddTenancy();

// or supply a custom resolver:
builder.Services.AddTenancy(options =>
    options.UseResolver<MyHeaderTenantResolver>());

2. Add middleware (after authentication)

app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<TenantResolutionMiddleware>();

3. Consume the tenant context

public class MyService(ITenantContext tenantContext)
{
    public Guid GetCurrentTenant() => tenantContext.TenantId
        ?? throw new InvalidOperationException("No tenant resolved.");
}

4. Enrich logs with tenant info

var tenantEnricher = app.Services.GetRequiredService<TenantLogEnricher>();
var userEnricher   = app.Services.GetRequiredService<UserLogEnricher>();

Log.Logger = new LoggerConfiguration()
    .Enrich.With(tenantEnricher)
    .Enrich.With(userEnricher)
    .WriteTo.Console()
    .CreateLogger();

Every log event will include TenantId, TenantName, and UserId properties.

Custom Tenant Resolver

Implement ITenantResolver to resolve the tenant from any source:

public class HeaderTenantResolver : ITenantResolver
{
    public Task<TenantInfo?> ResolveAsync(HttpContext context)
    {
        var raw = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
        if (raw is null || !Guid.TryParse(raw, out var id))
            return Task.FromResult<TenantInfo?>(null);

        return Task.FromResult<TenantInfo?>(new TenantInfo(id));
    }
}

Register it via AddTenancy():

// By type — HeaderTenantResolver is resolved from DI, so its own dependencies are injected
services.AddTenancy(options => options.UseResolver<HeaderTenantResolver>());

// Or with a factory when you need manual composition
services.AddTenancy(options => options.UseResolver(sp =>
    new HeaderTenantResolver(sp.GetRequiredService<IMyService>())));

Extending TenantInfo

TenantInfo is an open record — subclass it to carry additional metadata that your resolver provides and your application needs:

public record MyTenantInfo(Guid TenantId, string Region, string Plan, string? TenantName = null)
    : TenantInfo(TenantId, TenantName);

Return it from your resolver:

return new MyTenantInfo(tenantId, region: "eu-west", plan: "enterprise");

Access the extra fields from ITenantContext.TenantInfo:

if (tenantContext.TenantInfo is MyTenantInfo info)
{
    var region = info.Region;
    var plan   = info.Plan;
}

TenantId and TenantName remain available directly on ITenantContext as a convenience — no cast needed for standard properties.

JWT Claim Resolution (default)

When no custom resolver is configured, ClaimsTenantResolver reads from the following claims:

Claim Description
tid Primary tenant identifier (Azure AD / Entra ID)
tenantid Fallback tenant identifier
tenant_name Optional human-readable tenant name
sub Subject claim for user identity (UserLogEnricher)
NameIdentifier Fallback for user identity (UserLogEnricher)

Notes

  • No EF Core dependency — EF global query filters belong in Clywell.Core.Data.EntityFramework; this package is pure plumbing.
  • Opt-in middlewareAddTenancy() does not add middleware automatically; you must call app.UseMiddleware<TenantResolutionMiddleware>().
  • Scoped by designITenantContext is Scoped and populated once per HTTP request.
  • TryAdd semantics — calling AddTenancy() multiple times is safe; subsequent calls are no-ops for already-registered services.

License

MIT — see LICENSE for details.

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

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 0 3/1/2026