.NET Core  

Multi-Tenant SaaS Applications in ASP.NET Core

Introduction

As Software-as-a-Service (SaaS) continues to dominate the tech landscape, building applications that serve multiple customers (tenants) efficiently from a single codebase becomes essential. ASP.NET Core, with its modular and scalable architecture, is a powerful framework for implementing multi-tenant SaaS applications.

This article will guide you through core principles, patterns, and a practical approach to building a multi-tenant ASP.NET Core app.

What is Multi-Tenancy?

In a multi-tenant architecture, a single application instance serves multiple tenants, with each tenant having isolated data and configurations. There are three common multi-tenancy models.

  1. Single Database, Shared Schema: All tenants share the same database and tables. Tenant data is filtered per request.
  2. Single Database, Separate Schema per Tenant: One database, but each tenant has its own schema.
  3. Separate Database per Tenant: Each tenant has a completely separate database. Most secure, but also complex.

Core Components of a Multi-Tenant ASP.NET Core App

1. Tenant Identification Middleware

You must identify the tenant from the request, commonly through.

  • Subdomain (e.g., tenant1.app.com)
  • Header (e.g., X-Tenant-ID)
  • URL path (e.g., /tenant1/dashboard)
public class TenantResolutionMiddleware
{
    private readonly RequestDelegate _next;

    public TenantResolutionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, ITenantService tenantService)
    {
        var tenantId = context.Request.Headers["X-Tenant-ID"].FirstOrDefault();
        if (string.IsNullOrEmpty(tenantId))
        {
            context.Response.StatusCode = 400; // Bad Request
            await context.Response.WriteAsync("Tenant ID missing.");
            return;
        }

        await tenantService.SetCurrentTenantAsync(tenantId);
        await _next(context);
    }
}

2. Tenant Context & Service

Create a scoped service to store tenant information per request.

public interface ITenantService
{
    TenantInfo CurrentTenant { get; }
    Task SetCurrentTenantAsync(string tenantId);
}

public class TenantService : ITenantService
{
    public TenantInfo CurrentTenant { get; private set; }

    public Task SetCurrentTenantAsync(string tenantId)
    {
        // Fetch tenant info from a DB or config
        CurrentTenant = new TenantInfo { Id = tenantId, Name = $"Tenant {tenantId}" };
        return Task.CompletedTask;
    }
}

3. Data Isolation Using EF Core

Use a global query filter in Entity Framework Core to isolate data.

public class AppDbContext : DbContext
{
    private readonly ITenantService _tenantService;

    public AppDbContext(DbContextOptions<AppDbContext> options, ITenantService tenantService)
        : base(options)
    {
        _tenantService = tenantService;
    }

    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var tenantId = _tenantService.CurrentTenant?.Id;
        modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == tenantId);
    }
}

Managing Per-Tenant Configuration

Store configurations like feature toggles, limits, or branding in a TenantSettings table, and cache them using a memory cache or Redis.

public class TenantInfo
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ThemeColor { get; set; }
    public string DbConnectionString { get; set; } // for DB-per-tenant model
}

Tenant-Based Dependency Injection

ASP.NET Core allows scoped services per request; use it to register tenant-specific services dynamically if needed.

Authentication & Authorization

Use IdentityServer or JWT tokens that carry the tenant ID. This ensures that tokens are scoped to a particular tenant and can’t be used across them.

Testing and Debugging Multi-Tenant Logic

  • Use integration tests to simulate tenant-specific scenarios.
  • Enable verbose logging during tenant resolution.
  • Protect tenant boundaries using middleware and filters.

Best Practices

  • Isolate tenant data at all layers, including caching and messaging.
  • Use caching wisely, with tenant-scoped keys.
  • Implement strict authorization and rate limits per tenant.
  • Monitor and log tenant-specific performance metrics.

Conclusion

Building a multi-tenant SaaS app in ASP.NET Core isn’t just about shared data. It’s about smart architecture, secure isolation, and tenant-aware design patterns. By combining middleware, scoped services, EF Core filters, and modern deployment practices, you can scale your SaaS app with confidence.

Happy coding!