Building Multi-Tenant SaaS Applications with ASP.NET Core

Learn to build scalable multi-tenant SaaS apps with ASP.NET Core. Explore data isolation, tenant resolution, security, and deployment strategies with code examples.

SaaS ASP.NET Core Multi-tenant

Building Multi-Tenant SaaS Applications with ASP.NET Core

  • Facile Technolab Team
  • Sunday, May 25, 2025

Learn to build scalable multi-tenant SaaS apps with ASP.NET Core. Explore data isolation, tenant resolution, security, and deployment strategies with code examples.

Introduction to Multi-Tenant SaaS Applications

Multi-tenant Software-as-a-Service (SaaS) applications are designed to serve multiple customers (tenants) from a single codebase and infrastructure, while keeping their data and configurations isolated. This architecture optimizes resource usage, simplifies maintenance, and scales efficiently—making it a cornerstone of modern SaaS platforms like Salesforce, Shopify, and HubSpot.

In this guide, we’ll explore how to build a robust multi-tenant SaaS application using ASP.NET Core, covering architectural patterns, data isolation, security, scaling, and deployment. Whether you’re launching a B2B platform or a customer-facing tool, this guide provides actionable insights to navigate the complexities of multi-tenancy.

Architectural Considerations

What is Multi-Tenancy?

Multi-tenancy allows a single application instance to serve multiple tenants, each with isolated data, configurations, and user management. Key benefits include:

  • Cost Efficiency: Shared infrastructure reduces operational expenses.

  • Simplified Updates: Roll out features to all tenants simultaneously.

  • Scalability: Add tenants without spinning up new instances.

Multi-Tenancy Models

ModelDescriptionProsCons
Database per TenantSeparate database for each tenant.Strong data isolation.High operational overhead.
Shared Database, Separate SchemaSingle database with tenant-specific schemas (e.g., tenant1.orders).Moderate isolation.Complex migrations.
Shared Database, Shared SchemaSingle database/schema with tenant IDs segregating data (e.g., TenantId column).Low cost, simple setup.Risk of data leakage.

Recommendation: Start with a Shared Database, Shared Schema for simplicity, then evolve as needed.


Setting Up ASP.NET Core for Multi-Tenancy

Project Structure

Organize your solution to support tenant-aware services:

MySaaSApp/ 
├─ Tenants/ 
│ 
├─ Tenant.cs # Tenant model 
│ 
├─ ITenantService.cs # Tenant resolution logic 
├─ Data/ 
│ 
├─ AppDbContext.cs # EF Core DbContext with tenant filtering 
├─ Middleware/ 
│ 
├─ TenantMiddleware.cs # Resolves tenant from request

Tenant Identification Strategies

  • Subdomain: tenant1.your-app.com

  • Request Header: X-Tenant-Id: abc123

  • JWT Claim: "tenant": "tenant1"

  • Path Parameter: /api/tenant1/orders

Example: Tenant Middleware

public class TenantMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
    {
        // Extract tenant ID from subdomain (e.g., "tenant1.your-app.com")
        var subdomain = context.Request.Host.Host.Split('.')[0];
        var tenant = await tenantService.GetTenantByIdAsync(subdomain);

        if (tenant == null)
        {
            context.Response.StatusCode = 404;
            await context.Response.WriteAsync("Tenant not found");
            return;
        }

        // Store tenant in HttpContext for downstream use
        context.Items["Tenant"] = tenant;
        await _next(context);
    }
}

// Register middleware in Program.cs:
app.UseMiddleware<TenantMiddleware>();

Data Isolation with Entity Framework Core

Global Query Filters

Add tenant-specific filtering to EF Core models:

public class AppDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
    private string TenantId { get; set; }

    public AppDbContext(DbContextOptions options, ITenantContext tenantContext)
        : base(options)
    {
        TenantId = tenantContext.TenantId;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Apply global query filter to all entities with a TenantId property
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => o.TenantId == TenantId);
    }
}

// Example entity with TenantId:
public class Order
{
    public int Id { get; set; }
    public string TenantId { get; set; } // Tenant identifier
    public decimal Total { get; set; }
}

Tenant-Specific Migrations

Use separate migrations for shared and tenant-specific schemas:

dotnet ef migrations AddSharedSchema  
dotnet ef migrations AddTenantSchema --context TenantDbContext  

Authentication & Authorization

Multi-Tenant User Management

  • ASP.NET Core Identity: Extend IdentityUser with a TenantId property.

  • Roles & Claims: Assign tenant-specific roles (e.g., TenantAdmin).

Example: Tenant User Model

public class AppUser : IdentityUser { public string TenantId { get; set; } }

Policy-Based Authorization

public class AppUser : IdentityUser
{
    public string TenantId { get; set; } // Link users to tenants
}

// Policy-based authorization:
services.AddAuthorization(options =>
{
    options.AddPolicy("TenantAdmin", policy =>
        policy.RequireClaim("TenantId", context.Tenant.Id)
              .RequireRole("Admin"));
});

Scaling & Performance Optimization

Caching Tenant Data

Use Redis to cache tenant configurations:

// Register Redis in Program.cs:
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});

// Tenant service with caching:
public class TenantService
{
    private readonly IDistributedCache _cache;
    private readonly AppDbContext _context;

    public TenantService(IDistributedCache cache, AppDbContext context)
    {
        _cache = cache;
        _context = context;
    }

    public async Task<Tenant> GetTenantAsync(string tenantId)
    {
        var cachedTenant = await _cache.GetStringAsync($"tenant_{tenantId}");
        
        if (!string.IsNullOrEmpty(cachedTenant))
        {
            return JsonSerializer.Deserialize<Tenant>(cachedTenant);
        }

        var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId);
        await _cache.SetStringAsync($"tenant_{tenantId}", JsonSerializer.Serialize(tenant), 
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });

        return tenant;
    }
}

Database Sharding

Use libraries like ShardingCore to distribute tenants across databases.

Security Best Practices

  1. Data Encryption: Encrypt sensitive fields (e.g., TenantId) using AES or Azure Key Vault.

  2. Row-Level Security: Implement via SQL Server or PostgreSQL policies.

  3. Audit Logs: Track tenant activity with tools like Audit.NET.

Monitoring & Logging

Tenant-Aware Logging

Configure Serilog to include TenantId in logs:

// Configure Serilog in Program.cs:
Log.Logger = new LoggerConfiguration()
    .Enrich.WithProperty("TenantId", tenantContext.TenantId)
    .WriteTo.Console()
    .CreateLogger();

// Inject ILogger in services:
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    private readonly ITenantContext _tenantContext;

    public OrderService(ILogger<OrderService> logger, ITenantContext tenantContext)
    {
        _logger = logger;
        _tenantContext = tenantContext;
    }

    public void ProcessOrder()
    {
        _logger.LogInformation("Processing order for tenant {TenantId}", _tenantContext.TenantId);
    }
}

Conclusion

Building multi-tenant SaaS applications with ASP.NET Core requires careful planning around data isolation, tenant resolution, and scalability. By leveraging EF Core, middleware, and modern cloud tools, you can create a secure, efficient platform ready to scale with your user base.

Brick SaaS Starter Kit helps you get started with your Multi-tenant applications with asp.net core:

🔗 Website: Brick SaaS Starter Kit

Brick SaaS Starter Kit helps early-stage SaaS Founders save time and money by providing ready-to-use features that comes with the most common must-have features. You can consider this Angular SaaS Boilerplate or React SaaS Boilerplate as Brick has options to use Angular or React in front end while using asp.net core as back end APIs.

Here is the feature list:

  • Account Management - Registration, Login, Forgot Password, Reset Password, My Profile, Change Password
  • Tenant Management - Ability for service provider to manage tenants.
  • Authentication - Email authentication, Azure Authentication, and Social Authentication
  • Authorization - Role and Permission based authorization support
  • Multi-factor Authentication - Microsoft & Google Authenticator App Support, Email & SMS Code verification support
  • Payments - Subscription-based recurring payments through stripe account (Demo only)
  • Emails - Transactional emails through SMTP or Sendgrid
  • Emails Template Management - manage email templates of transaction emails to easily customize it.
  • Multi-language support
  • Data Encryption (Settings and Database data)
Contact Facile Team

Signup for monthly updates and stay in touch!

Subscribe to Facile Technolab's monthly newsletter to receive updates on our latest news, offers, promotions, resources, source code, jobs and other exciting updates.