Skip to main content
Version: 0.6.1

Multi-Tenant

Sharkable provides an opt-in multi-tenant resolution system. A scoped ITenant service is injected into any class that needs tenant awareness, and the tenant identifier is resolved per-request via configurable middleware.

Quick Start

builder.Services.AddShark(opt =>
{
opt.ConfigureMultiTenant(cfg =>
{
cfg.ResolveTenant = ctx =>
TenantResolver.FromHost(ctx)
?? TenantResolver.FromClaim(ctx);
});
});

var app = builder.Build();
app.UseShark(); // wires TenantResolutionMiddleware automatically

Inject ITenant anywhere:

public class OrderService
{
private readonly ITenant _tenant;

public OrderService(ITenant tenant)
{
_tenant = tenant;
}

public async Task<List<Order>> GetOrdersAsync()
{
var tenantId = _tenant.TenantId;
// filter by tenantId for data isolation
}
}

Resolving the Tenant

Tenant identity is resolved once per request by TenantResolutionMiddleware, then stored on the scoped ITenant for the remainder of the request.

The middleware checks TenantOptions.ResolveTenant at the start of each request. Set this delegate via ConfigureMultiTenant().

Built-in Resolvers

// Subdomain: tenant1.myapp.com → "tenant1"
TenantResolver.FromHost(HttpContext)

// JWT claim: reads "tenant_id" claim (customizable)
TenantResolver.FromClaim(HttpContext, claimType: "tenant_id")

Chain them with ?? for fallback:

cfg.ResolveTenant = ctx =>
TenantResolver.FromHost(ctx)
?? TenantResolver.FromClaim(ctx, "tenant_id");

Custom Lambda

For any other strategy, provide your own delegate:

cfg.ResolveTenant = ctx =>
{
var header = ctx.Request.Headers["X-Tenant"].FirstOrDefault();
return !string.IsNullOrEmpty(header) ? header : null;
};

Configuration

opt.ConfigureMultiTenant(cfg =>
{
// Func<HttpContext, string?> — return null when unresolvable
cfg.ResolveTenant = ctx => TenantResolver.FromClaim(ctx);
});

Behavior

SituationResult
Resolver returns a valueITenant.TenantId is set for the request
Resolver returns nullITenant.TenantId is null; the application handles it
No ConfigureMultiTenant callMiddleware is not wired; no tenant resolution
Multiple resolvers chainedFirst non-null result wins

Data Source Isolation

Route database connections per-tenant via ITenantDataSource. Inject it anywhere to get the correct connection string for the current request:

builder.Services.AddShark(opt =>
{
opt.ConfigureMultiTenant(cfg =>
{
cfg.ResolveTenant = ctx => TenantResolver.FromHost(ctx);

cfg.ConfigureDataSource(o =>
{
o.ConnectionStringResolver = tenantId =>
$"Server=db-{tenantId}.internal;Database=myapp";
});
});
});

// In any scoped service:
public class OrderService
{
private readonly ITenantDataSource _ds;
public OrderService(ITenantDataSource ds) => _ds = ds;

public void Connect()
{
var connStr = _ds.GetConnectionString(); // auto-resolved per tenant
}
}

The ITenantDataSource is a scoped service. It resolves the current tenant from ITenant, then maps it to a connection string via ConnectionStringResolver. Returns null when no tenant is resolved.

NuGet plugins (e.g., Sharkable.AutoCrud.SqlSugar) can wire ITenantDataSource into their DB client registration so that injected clients automatically point to the correct tenant database — with zero user code changes.

AOT Support

Fully AOT-safe. No reflection, no dynamic. The middleware resolves tenant via a Func<HttpContext, string?> delegate, and ITenant / Tenant are plain interfaces and classes.