Skip to content

Atypical-Consulting/TenantKit

Repository files navigation

🏢 TenantKit

Build License: MIT NuGet .NET

Zero-friction multi-tenancy middleware for ASP.NET Core.
Built by Atypical Consulting.


Table of Contents


The Problem

Every .NET SaaS application hits the multi-tenancy wall sooner or later.
Existing solutions like Finbuckle.MultiTenant are powerful but heavy — they're deeply coupled to Entity Framework, assume you want per-tenant databases, and require significant configuration before you get a tenant in scope.

You shouldn't need to fight your framework to know who's asking.


The Solution

TenantKit resolves the current tenant in 3 lines:

// Program.cs
builder.Services.AddTenantKit(tk => tk
    .UseHeaderResolver()
    .UseQueryStringResolver()
    .WithInMemoryStore(s => {
        s.Add(new Tenant { Id = "acme", Name = "Acme Corp" });
        s.Add(new Tenant { Id = "globex", Name = "Globex Inc." });
    }));

app.UseTenantKit();

Then inject ITenantContext anywhere:

app.MapGet("/hello", (ITenantContext ctx) =>
    $"Hello from tenant: {ctx.CurrentTenant?.Name ?? "unknown"}");

Features

  • Header-based tenant resolution (X-Tenant-Id)
  • Query string tenant resolution (?tenantId=)
  • Subdomain-based tenant resolution
  • JWT/cookie claim-based tenant resolution
  • Route value tenant resolution
  • Composite resolver (try multiple strategies in order)
  • In-memory tenant store
  • Scoped per-request ITenantContext
  • Fluent builder API for configuration
  • Custom ITenantStore support (bring your own data source)
  • Custom ITenantResolver support
  • Typed exceptions (TenantNotFoundException, TenantResolutionException)
  • EF Core integration (planned)
  • Redis-backed distributed tenant store (planned)
  • Per-tenant rate limiting (planned)
  • Per-tenant configuration overrides (planned)

Tech Stack

Layer Technology
Runtime .NET 10.0
Framework ASP.NET Core
Language C# 13
Testing xUnit
CI GitHub Actions

Packages

Package Description NuGet
TenantKit.Core Contracts + InMemoryTenantStore NuGet
TenantKit.AspNetCore Middleware, resolvers, DI extensions NuGet

Quick Start

Install

dotnet add package TenantKit.AspNetCore

Configure

builder.Services.AddTenantKit(tk => tk
    .UseHeaderResolver()          // X-Tenant-Id header
    .UseQueryStringResolver()     // ?tenantId=acme
    .WithInMemoryStore(s => {
        s.Add(new Tenant { Id = "acme", Name = "Acme Corp" });
    }));

app.UseTenantKit();

Use

// In a controller or minimal API
public class OrdersController : ControllerBase
{
    public OrdersController(ITenantContext tenantContext) { ... }

    [HttpGet]
    public IActionResult Get()
    {
        var tenant = _tenantContext.CurrentTenant;
        // Filter data by tenant.Id
    }
}

Available Resolvers

Resolver Looks at Example
HeaderTenantResolver X-Tenant-Id request header X-Tenant-Id: acme
QueryStringTenantResolver tenantId query param ?tenantId=acme
SubdomainTenantResolver First subdomain segment acme.myapp.com
ClaimTenantResolver JWT/cookie claim tenant_id claim
RouteValueTenantResolver Route parameter /{tenantId}/orders
CompositeTenantResolver Try resolvers in order First non-null wins

Configure multiple resolvers — they're tried in registration order:

builder.Services.AddTenantKit(tk => tk
    .UseHeaderResolver()
    .UseSubdomainResolver()
    .UseClaimResolver(claimType: "tenant_id")
    .WithInMemoryStore(...));

Core Contracts

public interface ITenant
{
    string Id { get; }
    string Name { get; }
}

public interface ITenantStore
{
    Task<ITenant?> FindByIdAsync(string tenantId, CancellationToken ct = default);
    Task<IReadOnlyList<ITenant>> GetAllAsync(CancellationToken ct = default);
}

public interface ITenantContext
{
    ITenant? CurrentTenant { get; }
}

Architecture

Request
  │
  ▼
TenantMiddleware
  │  calls resolvers in order
  ├─► HeaderTenantResolver
  ├─► QueryStringTenantResolver
  └─► ... (first non-null tenantId wins)
       │
       ▼
    ITenantStore.FindByIdAsync(tenantId)
       │
       ▼
    HttpTenantContext (scoped, per-request)
       │
       ▼
Downstream handlers / controllers / services

Project Structure

TenantKit/
├── src/
│   ├── TenantKit.Core/
│   │   ├── ITenant.cs
│   │   ├── ITenantStore.cs
│   │   ├── ITenantContext.cs
│   │   ├── ITenantResolver.cs
│   │   ├── Tenant.cs
│   │   ├── InMemoryTenantStore.cs
│   │   ├── TenantNotFoundException.cs
│   │   └── TenantResolutionException.cs
│   └── TenantKit.AspNetCore/
│       ├── Resolvers/
│       │   ├── HeaderTenantResolver.cs
│       │   ├── QueryStringTenantResolver.cs
│       │   ├── SubdomainTenantResolver.cs
│       │   ├── ClaimTenantResolver.cs
│       │   ├── RouteValueTenantResolver.cs
│       │   └── CompositeTenantResolver.cs
│       ├── TenantMiddleware.cs
│       ├── HttpTenantContext.cs
│       ├── TenantKitBuilder.cs
│       ├── TenantKitOptions.cs
│       ├── ServiceCollectionExtensions.cs
│       └── ApplicationBuilderExtensions.cs
├── tests/
│   └── TenantKit.Tests/         # 24/24 tests passing ✅
└── samples/
    └── TenantKit.Demo/          # Minimal API sample

Roadmap

  • EF Core integrationTenantKit.EntityFrameworkCore — per-tenant DbContext factory
  • Redis cacheTenantKit.Redis — distributed tenant store backed by Redis
  • Per-tenant rate limitingTenantKit.RateLimiting — ASP.NET Core rate limiter integration
  • Per-tenant configuration — override IConfiguration per tenant
  • NuGet publish — to nuget.org once API is stable

Stats

Alt


Contributing

Contributions are welcome! Please open an issue first for large changes.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit using conventional commits (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © 2026 Atypical Consulting


Built with care by Atypical Consulting — opinionated, production-grade open source.

Contributors

About

Zero-friction multi-tenancy middleware for ASP.NET Core

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages