Azure  

Designing Multi-Tenant Architecture in ASP.NET Core using EF Cor

Multi-tenancy is a powerful architectural pattern where a single application instance serves multiple customers (tenants), each logically isolated from the other. This approach is widely adopted in SaaS (Software-as-a-Service) solutions for efficiency, scalability, and cost optimization.

In this article, we will explore how to design and implement multi-tenant architecture in ASP.NET Core using Entity Framework Core (EF Core) focusing on strategies for tenant identification, database isolation, and data management.

1. Understanding Multi-Tenancy Models

Before jumping into implementation, it’s important to understand the common types of multi-tenancy:

a) Single Database, Shared Schema

All tenants share the same database and tables. Tenant data is distinguished by a TenantId column in each table.

  • Pros: Simple to maintain, cost-effective.

  • Cons: Limited data isolation, higher risk of cross-tenant data leaks.

b) Single Database, Separate Schemas

Each tenant has its own schema (like TenantA.Users, TenantB.Users).

  • Pros: Better isolation.

  • Cons: Harder to scale with a large number of tenants.

c) Separate Databases per Tenant

Each tenant has its own dedicated database.

  • Pros: Strong isolation and flexibility.

  • Cons: More complex management and higher cost.

2. Setting Up Tenant Identification Middleware

Every request must identify which tenant it belongs to. Common identification strategies include:

  • Subdomain-based (e.g., tenant1.app.com)

  • Path-based (e.g., app.com/tenant1)

  • Header-based (custom header like X-Tenant-ID)

Example: Tenant Identification Middleware

public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(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;
            await context.Response.WriteAsync("Tenant ID missing");
            return;
        }

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

And register it in Startup.cs or Program.cs:

app.UseMiddleware<TenantMiddleware>();

3. Creating the Tenant Service

This service manages the active tenant for the current request.

public interface ITenantService
{
    string GetTenant();
    void SetTenant(string tenantId);
}

public class TenantService : ITenantService
{
    private string _tenantId;
    public string GetTenant() => _tenantId;
    public void SetTenant(string tenantId) => _tenantId = tenantId;
}

Register it as a scoped service:

builder.Services.AddScoped<ITenantService, TenantService>();

4. Configuring EF Core for Multi-Tenancy

If using separate databases, dynamically build the connection string per tenant:

public class TenantDbContext : DbContext
{
    private readonly ITenantService _tenantService;
    private readonly IConfiguration _configuration;

    public TenantDbContext(DbContextOptions<TenantDbContext> options, ITenantService tenantService, IConfiguration configuration)
        : base(options)
    {
        _tenantService = tenantService;
        _configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var tenantId = _tenantService.GetTenant();
        var connectionString = _configuration.GetConnectionString("DefaultConnection").Replace("{tenantId}", tenantId);
        optionsBuilder.UseSqlServer(connectionString);
    }

    public DbSet<Customer> Customers { get; set; }
}

In your appsettings.json:

"ConnectionStrings": {
  "DefaultConnection": "Server=.;Database=Tenant_{tenantId};Trusted_Connection=True;"
}

5. Applying a Tenant Filter (Shared Schema Approach)

If using a shared schema, apply a global query filter:

public class ApplicationDbContext : DbContext
{
    private readonly ITenantService _tenantService;

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>().HasQueryFilter(c => c.TenantId == _tenantService.GetTenant());
    }

    public DbSet<Customer> Customers { get; set; }
}

This ensures that all queries automatically filter by the current tenant’s data.

6. Example Controller

[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    private readonly TenantDbContext _context;

    public CustomersController(TenantDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetCustomers()
    {
        var customers = await _context.Customers.ToListAsync();
        return Ok(customers);
    }
}

7. Practical Example in Action

Request

GET /api/customers
Header: X-Tenant-ID = TenantA

Database Connection Used

Server=.;Database=Tenant_TenantA;Trusted_Connection=True;

Result

Returns only data from TenantA’s database or filtered by TenantA’s TenantId.

8. Best Practices

  • Use caching for tenant configuration lookups.

  • Secure tenant isolation — never allow direct TenantId exposure in client-side apps.

  • Automate database provisioning using background jobs.

  • Use migrations per tenant if using isolated databases.

  • Regularly audit access and data isolation between tenants.

Conclusion

Implementing multi-tenancy in ASP.NET Core with EF Core enables scalability, cost efficiency, and easier maintenance for SaaS products. Depending on your business requirements whether lightweight shared databases or fully isolated ones — you can tailor this architecture to ensure each tenant’s data is secure, consistent, and high-performing.

A well-designed multi-tenant system not only reduces infrastructure costs but also provides a strong foundation for expanding your application to hundreds of tenants with minimal architectural change.