Multi-Tenancy Patterns in ASP.NET Core

2 minute read

Published:

This post covers common multi-tenancy patterns in ASP.NET Core. Multi-tenancy means one application serves multiple customers, organizations, or logical tenants while keeping their data and configuration separated. The hard parts are tenant identification, data isolation, configuration, security, and operations.

Tenant identification

The app needs a reliable way to determine the current tenant.

Common strategies:

  • subdomain: acme.example.com
  • route: /tenants/acme/orders
  • header: X-Tenant-Id
  • token claim: tenant_id

Example middleware concept:

public sealed class TenantMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context, ITenantResolver resolver)
    {
        var tenant = await resolver.ResolveAsync(context);

        if (tenant is null)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            await context.Response.WriteAsync("Tenant is required.");
            return;
        }

        context.Items["Tenant"] = tenant;
        await next(context);
    }
}

Tenant resolution must happen early enough that downstream services can use it.

Data isolation patterns

Common storage models:

  • shared database, shared schema, tenant column
  • shared database, separate schema per tenant
  • separate database per tenant

Shared database with tenant column:

  • simplest operationally
  • requires strict filters
  • works well for many small tenants

Separate database per tenant:

  • strongest isolation
  • easier tenant-level backup and restore
  • more operational complexity

There is no universal best option. The right model depends on tenant count, isolation needs, compliance, and operational maturity.

Tenant-aware EF Core queries

For shared-schema models, every tenant-owned row needs a tenant key.

public interface ITenantEntity
{
    string TenantId { get; set; }
}

Global query filters can help:

modelBuilder.Entity<Order>()
    .HasQueryFilter(o => o.TenantId == _tenantContext.TenantId);

This reduces accidental cross-tenant reads, but it is not a substitute for careful testing and authorization.

Tenant-specific configuration

Tenants may have different settings:

  • feature flags
  • branding
  • connection strings
  • limits and quotas
  • integration credentials

Model it explicitly:

public sealed record TenantSettings(
    string TenantId,
    bool EnableAdvancedReports,
    int MaxUsers);

Cache tenant settings carefully and invalidate them when changed.

Security concerns

Multi-tenancy raises the cost of mistakes. A cross-tenant data leak is a serious incident.

Controls:

  • authorize tenant membership from server-side identity
  • do not trust tenant IDs supplied by the client alone
  • add automated tests for tenant isolation
  • log tenant context in security events
  • avoid global admin shortcuts without auditing

Common mistakes to avoid

Watch for these issues:

  • forgetting tenant filters in one query
  • trusting headers from public clients without validation
  • mixing tenant configuration with user preferences
  • making tenant resolution inconsistent across APIs and workers
  • ignoring background jobs that process tenant data

Multi-tenancy must be a first-class architectural concern. Add tenant context, isolation, testing, and observability early, because retrofitting them later is expensive.


Next Article: Real-Time Systems in .NET: SignalR Architecture and Scaling