Kanject.Core.Tenancy 3.9.6

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

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.

⚠️ 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 Purpose
ITenantContext The one scoped service that exposes string TenantId — the single source of truth for the current tenant
TenantConstants TenantHeaderName = "x-tenant-id" — the canonical header, hoisted off 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.

Hoisting "x-tenant-id" onto TenantConstants follows the SDK's no-magic-strings rule — string literals that cross subsystem boundaries live on a *Constants type, never inline.


📖 Comprehensive Use Cases & Examples

1. Implement and register the context

Kanject.Core ships the contract; you own resolution. Implement ITenantContext for your edge (ASP.NET Core, Lambda, worker) 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>();

2. 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>();

3. Consume it downstream

Inject ITenantContext wherever a tenant boundary matters — services, repositories, query builders — and let DI hand you the right scoped instance.

public sealed class OrderRepository(ITenantContext tenant, IDbContext db)
{
    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 db.QueryByTenantAsync<Order>(tenant.TenantId, ct);
    }
}

4. 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.

// 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:

sealed file 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"), fakeDb);
    var orders = await repo.ListAsync(CancellationToken.None);
    Assert.All(orders, o => Assert.Equal("acme", o.TenantId));
}

🏗️ Architecture

Kanject.Core/Tenancy/
├── ITenantContext.cs              # string TenantId { get; } — register scoped
└── Constants/
    └── TenantConstants.cs         # const TenantHeaderName = "x-tenant-id"

Design notes:

  • Contract only — no implementation. Kanject.Core deliberately ships zero resolution logic. The edge (HTTP, Lambda, message envelope) is where tenant identity lives, and that's host-specific; binding the core SDK to HttpContext would couple every consumer to ASP.NET Core. You provide the implementation; the SDK 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.
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.9.6 44 7/18/2026
3.9.5 108 7/13/2026
3.9.4 132 7/11/2026
3.9.3 92 7/11/2026
3.9.2 232 7/9/2026