.NET 6 - How To Build Multitenant Application

What is Multitenancy?

A multitenant focuses on sharing these components fully or partially among the users.

In Addition, the application tier is usually scaled up vertically by adding more resources depending on the tenant’s preference and data usage volume.

Why Multitenancy?

For example,

One of the famous sneakers company has multiple branches called Branch A, Branch B, and Branch C. Each branch have its own inventory and budget.

Traditional Approach is to install a standalone app instance for each branch. In future, if you have extra branches D and E then we will deploy new branches same as old. But this is highly not efficient as there is no comman functional change in all these instances of the app.

Multitenancy Approach where each branch is a tenant with its own dataset. This allows the installation of the software once in a single location and scales up with new branches

Let’s assume Branch A runs with large Inventory and heavy workload whereas Branch B and Branch C operate with low budgets and few inventories. So, a shared database is a cost-effective choice for Branch B and Branch C.

You can better understand with below diagrams,

.NET 6 - How To Build Multitenant Application

Let's start with practical implementation,

Create Domain Class with primary key with tenant id.

public abstract class BaseEntity
{
    public int Id { get; set; }
    public string TenantId { get; set; }
}

The inventory class:

public class Inventory: BaseEntity
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

To define the necessary settings and data for tenants below model is very useful.

public class Tenant
{
    public string Name { get; set; } 
    public string Password { get; set; }
    public string ConnectionString { get; set; }
}

public class User
{
    public string Name { get; set; } = null!;
    public string Password { get; set; } = null!;
    public string TenantId { get; set; } = null!;
}

public class TenantScales
{
    public string? DefaultConnection { get; set; }
    public Tenant[] Tenants { get; set; } = Array.Empty<Tenant>();
    public User[] Users { get; set; } = Array.Empty<User>();
}

Here is the configuration file,

{
  "AllowedHosts": "*",
  "TenantScales": {
    "DefaultConnection": "Data Source=DESKTOP-60RDDSL;Initial Catalog=SneakersInventory;Integrated Security=True;MultipleActiveResultSets=True",
    "Tenants": [
      {
        "Name": "SneakersInventoryBranchA",
        "ConnectionString": "Data Source=DESKTOP-60RDDSL;Initial Catalog=SneakersInventoryBranchA;Integrated Security=True;MultipleActiveResultSets=True"
      },
      {
        "Name": "SneakersInventoryBranchB",
        "ConnectionString": "Data Source=DESKTOP-60RDDSL;Initial Catalog=SneakersInventoryBranchBC;Integrated Security=True;MultipleActiveResultSets=True"
      },
      {
        "Name": "SneakersInventoryBranchC",
        "ConnectionString": "Data Source=DESKTOP-60RDDSL;Initial Catalog=SneakersInventoryBranchBC;Integrated Security=True;MultipleActiveResultSets=True"
      }
    ],
    "Users": [
      {
        "Name": "SneakersInventoryBranchA-User",
        "Password": "SneakersInventoryBranchA-Password",
        "TenantId": "SneakersInventoryBranchA"
      },
      {
        "Name": "SneakersInventoryBranchB-User",
        "Password": "SneakersInventoryBranchB-Password",
        "TenantId": "SneakersInventoryBranchB"
      },
      {
        "Name": "SneakersInventoryBranchC-User",
        "Password": "SneakersInventoryBranchC-Password",
        "TenantId": "SneakersInventoryBranchC"
      }
    ]
  }
}

Here we have Users as well as tenants with its connection strings. tenant defines branches of sneakers shops.

Implement TenantRegistry method from ITenantRegistry interface.

namespace SneakersApplication.Infrastructure;
public class TenantRegistry: ITenantRegistry {
    private readonly TenantScales _tenantScales;
    public TenantRegistry(IConfiguration configuration) {
        _tenantScales = configuration.GetSection("TenantScales").Get < TenantScales > ();
        foreach(var tenant in _tenantScales.Tenants.Where(e => string.IsNullOrEmpty(e.ConnectionString))) {
            tenant.ConnectionString = _tenantScales.DefaultConnection;
        }
    }
    public Tenant[] GetTenants() => _tenantScales.Tenants;
    public User[] GetUsers() => _tenantScales.Users;
}

Implement TenantResolver method from ITenantResolver interface

namespace SneakersApplication.Infrastructure;
public class TenantResolver: ITenantResolver {
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITenantRegistry _tenantRegistry;
    public TenantResolver(IHttpContextAccessor httpContextAccessor, ITenantRegistry tenantRegistry) {
        _httpContextAccessor = httpContextAccessor;
        _tenantRegistry = tenantRegistry;
    }
    public Tenant GetCurrentTenant() {
        var claim = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(e => e.Type == ClaimConstants.TenantId);
        if (claim is null) throw new UnauthorizedAccessException("Authentication failed");
        var tenant = _tenantRegistry.GetTenants().FirstOrDefault(t => t.Name == claim.Value);
        if (tenant is null) throw new UnauthorizedAccessException($ "Tenant '{claim.Value}' is not registered.");
        return tenant;
    }
}

Now you can do code-first approach to create inventory class same as below snaps.

.NET 6 - How To Build Multitenant Application

First, we add an endpoint that will create inventory in the system before that we need to integrate token implementation to call inventory API.

Auth Token API

namespace SneakersApplication.Api.Controllers;
[ApiController]
[Route("[controller]")]
public class AuthController: ControllerBase {
    private readonly ITenantRegistry _tenantRegistry;
    public AuthController(ITenantRegistry tenantRegistry) {
            _tenantRegistry = tenantRegistry;
        }
        [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest loginRequest) {
        if (loginRequest is null) return BadRequest();
        var user = _tenantRegistry.GetUsers().FirstOrDefault(e => e.Name == loginRequest.UserName && e.Password == loginRequest.Password);
        if (user is null) return Unauthorized($ "Invalid user");
        if (_tenantRegistry.GetTenants().FirstOrDefault(e => e.Name == user.TenantId) is not {}
            tenant) return Unauthorized($ "Invalid tenant");
        var tokenString = JwtHelper.GenerateToken(tenant);
        return Ok(new {
            Token = tokenString
        });
    }
}
namespace SneakersApplication.Api.Controllers;
[ApiController]
[Route("[controller]")]
[Authorize]
public class InventoryController: ControllerBase {
    private readonly IInventoryRepository _InventoryRepository;
    public InventoryController(IInventoryRepository InventoryRepository) {
            _InventoryRepository = InventoryRepository;
        }
        [HttpPost]
    public async Task < IActionResult > AddAsync(InventoryDto InventoryDto) {
            var Inventory = await _InventoryRepository.AddAsync(InventoryDto);
            return Created(string.Empty, Inventory);
        }
        [HttpGet]
    public async Task < IActionResult > GetListAsync() {
        var list = await _InventoryRepository.GetAllAsync();
        return Ok(list);
    }
}

Now here is inventory APIs,

namespace SneakersApplication.Api.Controllers;
[ApiController]
[Route("[controller]")]
public class AuthController: ControllerBase {
    private readonly ITenantRegistry _tenantRegistry;
    public AuthController(ITenantRegistry tenantRegistry) {
            _tenantRegistry = tenantRegistry;
        }
        [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest loginRequest) {
        if (loginRequest is null) return BadRequest();
        var user = _tenantRegistry.GetUsers().FirstOrDefault(e => e.Name == loginRequest.UserName && e.Password == loginRequest.Password);
        if (user is null) return Unauthorized($ "Invalid user");
        if (_tenantRegistry.GetTenants().FirstOrDefault(e => e.Name == user.TenantId) is not {}
            tenant) return Unauthorized($ "Invalid tenant");
        var tokenString = JwtHelper.GenerateToken(tenant);
        return Ok(new {
            Token = tokenString
        });
    }
}

We have done with multitenant sneakers application.

Now let's do some testing.

.NET 6 - How To Build Multitenant Application

Here is full testing video of the multitenant sneakers application.

To conclude, In this article, we have learned how to build a multi-tenant application with .NET 6. We have also discussed various things as a comparison of traditional approaches.

Hope you guys learned something new. Keep learning!

Download the full source code of these three applications

Here are the links to my previous articles. 


Similar Articles