Isaac.

architecture

Multi-Tenancy in ASP.NET Core

Build scalable multi-tenant applications serving multiple customers.

By Emem IsaacFebruary 2, 20243 min read
#multi-tenancy#saas#aspnet core#architecture#scalability
Share:

A Simple Analogy

Multi-tenancy is like apartment building management. Multiple customers (tenants) share infrastructure but each has isolated data and configuration. One bill, many customers.


Why Multi-Tenancy?

  • Cost efficiency: Share infrastructure across customers
  • Scalability: Serve more customers with same resources
  • Revenue: SaaS pricing models
  • Simplicity: Single deployment for all customers
  • Customization: Per-tenant configuration

Tenant Identification

// Middleware to identify tenant
public class TenantResolutionMiddleware
{
    private readonly RequestDelegate _next;
    
    public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
    {
        // Get tenant from subdomain
        var host = context.Request.Host.Host;
        var tenantId = host.Split('.')[0]; // customer.example.com -> customer
        
        // Or from header
        if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var headerValue))
            tenantId = headerValue.ToString();
        
        // Or from route
        if (context.GetRouteData().Values.TryGetValue("tenantId", out var routeValue))
            tenantId = routeValue.ToString();
        
        var tenant = await tenantService.GetTenantAsync(tenantId);
        if (tenant == null)
            return;
        
        context.Items["Tenant"] = tenant;
        await _next(context);
    }
}

// Register
app.UseMiddleware<TenantResolutionMiddleware>();

Database Strategy

// Shared database with tenant column
public class Order
{
    public int Id { get; set; }
    public string TenantId { get; set; }  // Tenant isolation
    public string CustomerId { get; set; }
    public decimal Total { get; set; }
}

// Database per tenant
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var tenantId = _httpContextAccessor.HttpContext.Items["TenantId"].ToString();
    var connectionString = $"Server=localhost;Database=tenant_{tenantId};...";
    optionsBuilder.UseSqlServer(connectionString);
}

// Hybrid: Shared catalog, separate data databases
// One central database (catalogs, tenants, users)
// One database per tenant for their data

Query Filtering by Tenant

public class OrderRepository
{
    private readonly AppDbContext _context;
    private readonly ITenantContext _tenantContext;
    
    public async Task<List<Order>> GetAllAsync()
    {
        return await _context.Orders
            .Where(o => o.TenantId == _tenantContext.TenantId)  // Always filter
            .ToListAsync();
    }
}

// Or use interceptors
public class TenantInterceptor : SaveChangesInterceptor
{
    private readonly ITenantContext _tenantContext;
    
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        // Automatically add TenantId to new entities
        foreach (var entry in eventData.Context.ChangeTracker.Entries())
        {
            if (entry.Entity is ITenantEntity tenantEntity && entry.State == EntityState.Added)
            {
                tenantEntity.TenantId = _tenantContext.TenantId;
            }
        }
        
        return new ValueTask<InterceptionResult<int>>(result);
    }
}

Per-Tenant Configuration

public interface ITenant
{
    string Id { get; }
    string Name { get; }
    Dictionary<string, string> Settings { get; }
}

public class TenantService
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<Tenant> _repository;
    
    public async Task<ITenant> GetTenantAsync(string tenantId)
    {
        var cached = await _cache.GetStringAsync($"tenant_{tenantId}");
        if (!string.IsNullOrEmpty(cached))
            return JsonSerializer.Deserialize<Tenant>(cached);
        
        var tenant = await _repository.GetAsync(tenantId);
        
        if (tenant != null)
        {
            var json = JsonSerializer.Serialize(tenant);
            await _cache.SetStringAsync($"tenant_{tenantId}", json, 
                new DistributedCacheEntryOptions
                {
                    SlidingExpiration = TimeSpan.FromHours(1)
                });
        }
        
        return tenant;
    }
}

Dependency Injection Per Tenant

builder.Services.AddScoped(sp =>
{
    var httpContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
    var tenant = httpContext.Items["Tenant"] as ITenant;
    
    // Create configuration per tenant
    return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
        .UseSqlServer($"Server=localhost;Database=tenant_{tenant.Id};...")
        .Options);
});

Best Practices

  1. Prevent data leaks: Always filter by tenant
  2. Use interceptors: Auto-add tenant ID
  3. Cache tenant config: Reduce database queries
  4. Isolate databases: When compliance requires
  5. Monitor per-tenant: Track usage per customer

Related Concepts

  • Row-level security (RLS)
  • Tenant policies for authorization
  • Usage analytics per tenant
  • Custom theming per tenant

Summary

Multi-tenancy enables serving multiple customers efficiently from single infrastructure. Master data isolation, tenant identification, and per-tenant configuration to build scalable SaaS applications.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours