JG.TenantKit 1.0.0

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

JG.TenantKit

NuGet Downloads License CI

A production-ready multi-tenancy library for .NET 8 ASP.NET Core applications. Resolve tenants from headers, subdomains, routes, query strings, or JWT claims. Isolate data and configuration per tenant with scoped dependency injection.

Features

  • 5 Built-in Tenant Resolvers — Headers, subdomains, routes, query strings, JWT claims
  • Composable Resolution — Chain multiple resolvers with fallback support
  • Scoped Tenant Context — Access the current tenant via ITenantContext throughout your request
  • Pluggable Tenant Store — Implement ITenantStore for custom backends
  • 3 Built-in Stores — In-memory (dev), configuration-based (appsettings.json), or custom
  • Transparent Caching — TTL-based cache with case-insensitive lookups
  • Early Pipeline Resolution — Resolve tenant before reaching your handlers
  • Proper Error Handling — HTTP 400 (missing) / 403 (disabled) responses
  • Zero External Dependencies — Uses only Microsoft.Extensions and Microsoft.AspNetCore
  • Full Test Coverage — 40+ tests covering all scenarios

Installation

dotnet add package JG.TenantKit

Quick Start

1. Register Services

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddTenantKit(options =>
    {
        options.FallbackTenantId = "default";
    })
    .AddHeaderTenantResolver("X-Tenant-ID")
    .AddSubdomainTenantResolver()
    .AddInMemoryTenantStore(new Dictionary<string, TenantInfo>
    {
        ["acme"] = new("acme", "ACME Corp", isEnabled: true),
        ["globex"] = new("globex", "Globex Corp", isEnabled: true)
    });

var app = builder.Build();

// Add middleware early in pipeline
app.UseTenantResolution();

app.Run();

2. Use in Your Services

public class OrderService(ITenantContext tenantContext, ITenantStore store)
{
    public async Task<List<Order>> GetOrdersAsync()
    {
        if (!tenantContext.IsResolved)
            throw new InvalidOperationException("Tenant not resolved");

        var tenant = await store.GetByIdAsync(tenantContext.TenantId);
        // Use tenant.ConnectionString for data isolation
        
        return await LoadOrdersAsync(tenantContext.TenantId);
    }
}

3. Use Tenant Context in Controllers

[ApiController]
[Route("[controller]")]
public class OrdersController(ITenantContext tenantContext)
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(new { TenantId = tenantContext.TenantId });
    }
}

Tenant Resolvers

HeaderResolver

Extract tenant from HTTP header:

services.AddHeaderTenantResolver("X-Tenant-ID");
services.AddHeaderTenantResolver("X-Org");  // Custom header

SubdomainResolver

Extract tenant from subdomain (e.g., acme.example.com"acme"):

services.AddSubdomainTenantResolver();  // Ignores "www", "api" by default

// Custom ignored subdomains
var ignored = new HashSet<string> { "staging", "test" };
services.AddSubdomainTenantResolver(ignored);

RouteResolver

Extract tenant from first route segment (e.g., /acme/orders"acme"):

services.AddRouteTenantResolver();

QueryStringResolver

Extract tenant from query parameter:

services.AddQueryStringTenantResolver("tenant");
services.AddQueryStringTenantResolver("org");  // Custom parameter

ClaimResolver

Extract tenant from JWT claims:

services.AddClaimTenantResolver("tenant_id");
services.AddClaimTenantResolver("org_id");  // Custom claim

Composite Resolution

Chain multiple resolvers (first match wins):

services
    .AddTenantKit()
    .AddHeaderTenantResolver()       // Try header first
    .AddSubdomainTenantResolver()    // Then subdomain
    .AddRouteTenantResolver();       // Then route

Tenant Stores

InMemoryTenantStore

For development and testing:

var tenants = new Dictionary<string, TenantInfo>
{
    ["acme"] = new("acme", "ACME Corp", true, "Server=localhost;Database=acme"),
};

services.AddInMemoryTenantStore(tenants);

ConfigurationTenantStore

Read from appsettings.json:

{
  "Tenants": {
    "acme": {
      "DisplayName": "ACME Corp",
      "IsEnabled": true,
      "ConnectionString": "Server=acme.db;Database=acme",
      "Properties": {
        "PlanType": "Enterprise"
      }
    }
  }
}
services.AddConfigurationTenantStore();

Custom Store

Implement ITenantStore:

public class DatabaseTenantStore : ITenantStore
{
    public async ValueTask<TenantInfo?> GetByIdAsync(string tenantId, CancellationToken cancellationToken = default)
    {
        // Fetch from your database
        return await FetchTenantAsync(tenantId, cancellationToken);
    }
}

services.AddTenantStore<DatabaseTenantStore>();

Caching

Add transparent caching:

services
    .AddInMemoryTenantStore(tenants)
    .AddCaching(TimeSpan.FromMinutes(5));

Error Handling

Scenario Status Code
Tenant resolved successfully 200 OK
Tenant required but not found 400 Bad Request
Tenant disabled 403 Forbidden

Configuration

options.FallbackTenantId = "default";        // Optional fallback tenant
options.RequireResolution = true;            // Require tenant (400 if missing)
options.CacheTtl = TimeSpan.FromMinutes(5);  // Cache duration

Performance

  • Scoped Lifetime — One context per request
  • ValueTask — Zero allocations on cache hits
  • ConcurrentDictionary — Thread-safe caching
  • ConfigureAwait(false) — Library best practices
  • No LINQ — Efficient hot paths

License

Apache License 2.0. See LICENSE for details.

Contributing

Contributions welcome! Please open an issue or pull request.

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 was computed.  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 was computed.  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.
  • net8.0

    • No dependencies.

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 88 3/5/2026