JG.TenantKit
1.0.0
dotnet add package JG.TenantKit --version 1.0.0
NuGet\Install-Package JG.TenantKit -Version 1.0.0
<PackageReference Include="JG.TenantKit" Version="1.0.0" />
<PackageVersion Include="JG.TenantKit" Version="1.0.0" />
<PackageReference Include="JG.TenantKit" />
paket add JG.TenantKit --version 1.0.0
#r "nuget: JG.TenantKit, 1.0.0"
#:package JG.TenantKit@1.0.0
#addin nuget:?package=JG.TenantKit&version=1.0.0
#tool nuget:?package=JG.TenantKit&version=1.0.0
JG.TenantKit
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
ITenantContextthroughout your request - Pluggable Tenant Store — Implement
ITenantStorefor 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 | Versions 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. |
-
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 |