Kanject.Core.Tenancy 3.11.7

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

Kanject.Core.Tenancy

Kanject Tenancy — the minimal contract every multi-tenant Kanject service shares: a single scoped ITenantContext that answers "which tenant is this request for?", plus the one canonical header name everything agrees on.

Installation

dotnet add package Kanject.Core.Tenancy

Targets .NET 8, .NET 9 and .NET 10. The package has no dependencies and is Native AOT / trimming compatible. Kanject.Core.Api and Kanject.Core.ApiV2 already reference it, so ASP.NET Core services using either get it transitively.

⚠️ The Problem

Multi-tenancy leaks tenant identity into every layer, and without a shared contract it does so inconsistently:

  1. The tenant ID is resolved a dozen different ways. One handler reads a JWT claim, another a route value, a third an x-tenant header, a fourth X-TenantId. Casing and header names drift, and the data layer can't trust any single source.
  2. Tenant identity gets threaded by hand. Passing a tenantId string down through service → repository → query is noisy, easy to forget, and trivially mis-wired (last week's tenant captured in a closure, the wrong overload called).
  3. The header name is a magic string. "x-tenant-id" ends up hard-coded in the gateway, the middleware, the integration tests, and the docs — each a separate place to typo, each drifting independently.
  4. The persistence layer has no contract to bind to. A repository that wants to scope every query to the current tenant needs something stable to depend on — not an HttpContext it shouldn't see, and not a constructor string it can't get from DI.

🛠️ The Solution

Two tiny, dependency-free pieces:

Piece Namespace Purpose
ITenantContext Kanject.Core.Tenancy The one scoped service that exposes string TenantId — the single source of truth for the current tenant
TenantConstants Kanject.Core.Tenancy.Constants TenantHeaderName = "x-tenant-id" — the canonical header, as a constant instead of a magic string

The key principle: resolve the tenant once, at the edge, into a scoped ITenantContext; everything downstream depends on the interface, never on where the ID came from. Swap header-based resolution for a JWT claim without touching a single repository.


📖 Use Cases & Examples

1. Use the ready-made ASP.NET Core middleware

This package is the contract only. For HTTP services, Kanject.Core.Api (and Kanject.Core.ApiV2) provide the implementation. AddTenantMiddleware() registers a scoped ITenantContext, and UseTenantMiddleware() fills it from the x-tenant-id request header:

using Kanject.Core.Api.Extensions;

builder.Services.AddTenantMiddleware();

var app = builder.Build();

app.UseAuthentication();
app.UseTenantMiddleware();   // after authentication, before tenant-aware services run
app.UseAuthorization();

When the header is missing or blank, the middleware leaves TenantId empty, so tenant-neutral endpoints keep working. Services that require a tenant must check for that (see example 4).

2. Implement and register your own context

For any other edge (Lambda, a queue worker, a non-header HTTP scheme), implement ITenantContext yourself and register it scoped so each request/invocation gets its own tenant.

using Kanject.Core.Tenancy;
using Kanject.Core.Tenancy.Constants;

public sealed class HttpTenantContext : ITenantContext
{
    public HttpTenantContext(IHttpContextAccessor accessor)
    {
        // Resolve from the canonical header — no magic string.
        TenantId = accessor.HttpContext?
            .Request.Headers[TenantConstants.TenantHeaderName]
            .ToString() ?? string.Empty;
    }

    public string TenantId { get; }
}

// Startup — scoped, so it never bleeds across requests.
services.AddHttpContextAccessor();
services.AddScoped<ITenantContext, HttpTenantContext>();

3. Resolve from a JWT claim instead (no downstream changes)

Because everything depends on the interface, the resolution strategy is a single swap:

public sealed class ClaimsTenantContext(IHttpContextAccessor accessor) : ITenantContext
{
    public string TenantId =>
        accessor.HttpContext?.User.FindFirst("tenant_id")?.Value ?? string.Empty;
}

services.AddScoped<ITenantContext, ClaimsTenantContext>();

4. Consume it downstream

Inject ITenantContext wherever a tenant boundary matters — services, repositories, query builders — and let DI hand you the right scoped instance. IOrderStore below is a placeholder for your own data-access abstraction:

using Kanject.Core.Tenancy;

// Hypothetical data-access abstraction — stands in for whatever your app uses.
public interface IOrderStore
{
    Task<IReadOnlyList<Order>> ListForTenantAsync(string tenantId, CancellationToken ct);
}

public sealed class OrderRepository(ITenantContext tenant, IOrderStore store)
{
    public Task<IReadOnlyList<Order>> ListAsync(CancellationToken ct)
    {
        if (string.IsNullOrEmpty(tenant.TenantId))
            throw new InvalidOperationException("No tenant resolved for this request.");

        // Scope every query to the current tenant — partition key, filter, etc.
        return store.ListForTenantAsync(tenant.TenantId, ct);
    }
}

5. The header name, everywhere

Reference TenantConstants.TenantHeaderName from middleware, gateways, outbound clients, and tests so there is exactly one spelling of "x-tenant-id" in the system.

using Kanject.Core.Tenancy.Constants;

// Propagating the tenant to a downstream service call:
httpClient.DefaultRequestHeaders.Add(TenantConstants.TenantHeaderName, tenant.TenantId);

🧪 Testing

Because ITenantContext is a one-property interface, tests need no HTTP, no DI container, and no header plumbing — hand-roll a stub:

file sealed class FixedTenant(string id) : ITenantContext
{
    public string TenantId => id;
}

[Fact]
public async Task Repository_Scopes_To_Current_Tenant()
{
    var repo = new OrderRepository(new FixedTenant("acme"), fakeStore);
    var orders = await repo.ListAsync(CancellationToken.None);
    Assert.All(orders, o => Assert.Equal("acme", o.TenantId));
}

🏗️ Design notes

  • Contract only — no implementation. This package deliberately ships zero resolution logic. The edge (HTTP, Lambda, message envelope) is where tenant identity lives, and that's host-specific; binding the contract to HttpContext would couple every consumer to ASP.NET Core. You provide the implementation (or use the Kanject.Core.Api middleware); this package provides the seam.
  • Scoped lifetime is the contract's expectation. A singleton ITenantContext would bleed one request's tenant into another — the same class of cross-request state-sharing bug that affects any per-unit collaborator registered as a singleton. Register it scoped.
  • TenantId is non-null by interface. Implementations should return string.Empty (not null) when no tenant is resolvable, and tenant-scoped operations should reject empty up front rather than silently querying across tenants.
Package Role Availability
Kanject.Core.Api ASP.NET Core edge conventions, including AddTenantMiddleware() / UseTenantMiddleware(), which populate ITenantContext from the x-tenant-id header nuget.org
Kanject.Core.ApiV2 Same tenant middleware for the V2 API package nuget.org
Kanject.Core.NoSqlDatabase.Provider.DynamoDb DynamoDB provider; its generated [MultiTenant] repositories take an ITenantContext and scope data by TenantId Commercial license (not on nuget.org)

License

Licensed under the Kanject Code Libraries License Agreement (KCLLA); the full text ships in this package as LICENSE.md. Organizations whose trailing-twelve-month gross revenue and total funding raised are each below US$250,000 may use it at no cost under the Free Tier. At or above either threshold a commercial license is required — contact commercial@kanjectbusiness.com.

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 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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Kanject.Core.Tenancy:

Package Downloads
Kanject.Core.Api

Kanject core api

Kanject.Core.ApiV2

Kanject core api V2

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.12.1 152 9/27/2026
3.12.0 112 9/27/2026
3.11.7 117 9/26/2026
3.11.6 168 9/7/2026
3.11.5 143 8/27/2026
3.11.4 141 8/22/2026
3.11.3 157 8/10/2026
3.11.2 144 8/9/2026
3.11.1 146 8/5/2026
3.11.0 141 8/5/2026
3.10.0 156 8/3/2026
3.9.7 160 7/30/2026
3.9.6 167 7/18/2026
3.9.5 132 7/13/2026
3.9.4 163 7/11/2026
3.9.3 120 7/11/2026
3.9.2 266 7/9/2026