Skip to main content

Moongazing.MultiTenant

Moongazing.MultiTenant: Lightweight, flexible, production-ready multi-tenant framework for ASP.NET Core.

  • Strongly-typed pipeline (TenantId, ITenantContext)
  • Extensible resolution strategies (ITenantResolutionStrategy)
  • Zero reflection at runtime
  • Clean DI + middleware integration

Core + ASP.NET Core only. EF Core integration ships separately as Moongazing.MultiTenant.EFCore.


? Features​

  • ?? Header-based tenant resolution (default header X-Tenant-Id, configurable)
  • ?? Scoped ITenantContext (with strongly-typed TenantId)
  • ?? Tenant store abstraction (ITenantStore)
  • ?? In-memory tenant store implementation
  • ?? Pluggable resolution pipeline (strategies executed in registration order)
  • ?? Middleware for per-request resolution
  • ? No hidden static state / globals
  • ?? Designed to extend (EF Core, gRPC, custom stores, custom strategies)

?? Installation​

# From your ASP.NET Core project directory
dotnet add package Moongazing.MultiTenant

?? Quick Start​

1?? Register multi-tenant services

builder.Services.AddMoongazingMultiTenant(options =>
{
options.NotFoundMode = TenantNotFoundMode.Throw; // or UseDefault / Anonymous
options.HeaderName = "X-Tenant-Id"; // customize if needed
});

2?? Provide tenant definitions

var tenants = new[]
{
new TenantInfo
{
Id = new TenantId("tenant-a"),
Name = "Tenant A",
ConnectionString = "Host=...;Database=A;"
},
new TenantInfo
{
Id = new TenantId("tenant-b"),
Name = "Tenant B",
ConnectionString = "Host=...;Database=B;"
}
};

builder.Services.AddInMemoryTenantStore(tenants);

3?? Add middleware

app.UseMoongazingMultiTenant();

4?? Consume ITenantContext

app.MapGet("/whoami", (ITenantContext tenant) => new { Tenant = tenant.Id });

Request example:

GET /whoami
X-Tenant-Id: tenant-a

?? How It Works​

Resolution pipeline runs once per request:

[Strategy 1] ? [Strategy 2] ? [Strategy 3] ? First Match ? Resolved ITenantContext

Stops at first successful strategy.

Built-in strategy: HeaderTenantResolutionStrategy. Add your own (host, path, JWT claim, custom logic) by implementing the interface below.


?? Custom Resolution Strategy​

public sealed class CustomStrategy : ITenantResolutionStrategy
{
public Task<TenantResolutionResult> ResolveAsync(
TenantResolutionContext context,
CancellationToken cancellationToken = default)
{
var tenantId = context.Items.TryGetValue("Custom", out var value) ? value?.ToString() : null;
if (!string.IsNullOrWhiteSpace(tenantId))
{
return Task.FromResult(
TenantResolutionResult.FromSource(new TenantId(tenantId), "Custom")
);
}
return Task.FromResult(TenantResolutionResult.NotFound());
}
}

// Registration (order matters):
builder.Services.AddScoped<ITenantResolutionStrategy, CustomStrategy>();

?? TenantNotFoundMode​

ModeBehavior
ThrowThrows if tenant cannot be resolved (default)
UseDefaultUses DefaultTenantId if set
AnonymousContinues with unresolved context (IsResolved = false)
options.NotFoundMode = TenantNotFoundMode.UseDefault;
options.DefaultTenantId = new TenantId("tenant-a");

?? Key Abstractions​

public interface ITenantContext
{
bool IsResolved { get; }
TenantId Id { get; }
ITenantInfo Info { get; }
}

public interface ITenantStore
{
Task<ITenantInfo?> FindByIdAsync(TenantId id, CancellationToken ct = default);
Task<IReadOnlyList<ITenantInfo>> GetAllAsync(CancellationToken ct = default);
}

public sealed class MultiTenantOptions
{
public TenantNotFoundMode NotFoundMode { get; set; } = TenantNotFoundMode.Throw;
public TenantId? DefaultTenantId { get; set; }
public string? HeaderName { get; set; } = "X-Tenant-Id";
}

?? EF Core Integration (Separate Package)​

Planned features (in Moongazing.MultiTenant.EFCore):

  • Tenant-scoped DbContext base class
  • Per-tenant connection strings / schema
  • Global query filters
  • ITenantScoped marker interface

?? Testing​

// Direct construction
var tenantInfo = new TenantInfo { Id = new TenantId("test"), Name = "Test", ConnectionString = "..." };
var tenantContext = new TenantContext(tenantInfo);

// Mocking store (example using NSubstitute)
var store = Substitute.For<ITenantStore>();
store.FindByIdAsync(new TenantId("t1")).Returns(tenantInfo);

?? Roadmap​

v1.0.0

  • ? Header-based strategy
  • ? In-memory store
  • ? Tenant context + middleware
  • ? Namespace separation (Abstractions / Core / AspNetCore)

v1.1.0

  • Host-based resolution
  • Path-based resolution

v1.2.0

  • JWT claim resolution
  • Policy attributes ([TenantRequired], [TenantMatch])

v2.0

  • Multi-tenant health checks
  • Tenant feature flags
  • Redis tenant store

?? Contributing​

Contributions welcome.

Guidelines:

  • Follow .editorconfig
  • XML docs for public APIs
  • Unit tests for new logic
  • Meaningful commit messages

?? License​

MIT License. See LICENSE file.

Packages​

PackageVersionDownloads
Moongazing.MultiTenant1.0.0299