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
<PackageReference Include="Clywell.Core.Tenancy" Version="1.0.0" />
<PackageVersion Include="Clywell.Core.Tenancy" Version="1.0.0" />
<PackageReference Include="Clywell.Core.Tenancy" />
paket add Clywell.Core.Tenancy --version 1.0.0
#r "nuget: Clywell.Core.Tenancy, 1.0.0"
#:package Clywell.Core.Tenancy@1.0.0
#addin nuget:?package=Clywell.Core.Tenancy&version=1.0.0
#tool nuget:?package=Clywell.Core.Tenancy&version=1.0.0
Clywell.Core.Tenancy
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 carryingTenantId,TenantName,IsResolved, and the fullTenantInfoobject for the current requestITenantResolver— pluggable abstraction for tenant resolution; implement it to resolve from headers, subdomains, databases, API keys, or any custom sourceTenantInfo— extensible record carrying the result of a successful resolution; subclass it to carry additional tenant metadataClaimsTenantResolver— built-in resolver that readstid/tenantidJWT claims; used by default when no custom resolver is configuredTenantResolutionMiddleware— opt-in middleware that delegates toITenantResolverand populatesITenantContextper requestTenancyOptions— configuration object accepted byAddTenancy()for declaring a custom resolver at registration timeTenantLogEnricher— Serilog enricher that appendsTenantIdandTenantNameto every log eventUserLogEnricher— Serilog enricher that appendsUserIdfrom the authenticated user's claimsAddTenancy()— 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 middleware —
AddTenancy()does not add middleware automatically; you must callapp.UseMiddleware<TenantResolutionMiddleware>(). - Scoped by design —
ITenantContextisScopedand populated once per HTTP request. TryAddsemantics — callingAddTenancy()multiple times is safe; subsequent calls are no-ops for already-registered services.
License
MIT — see LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Clywell.Primitives (>= 1.1.0)
- Serilog (>= 4.3.1)
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 |