Change database connection based on path to support multitenancy

Posted on November 19, 2019

How to change the database connection string in the DbContext of an ASP.NET Core application based on the path.

In this post I want to show an implemention of a lightweight multitenancy solution with ef core. The solution will use a database per tenant and the database connection is set up on every request based on the path. The first part of the path is the tenant id and is used as the database name.

To receive the path of the current request the HttpContextAccessor has to be added to the services.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddHttpContextAccessor();
        services.AddDbContext<TenantDbContext>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // ...        
    }
}

Now the IHttpContextAccessor can be injected in the DBContext class. When the DBContext class configures the connection the tenant id is extracted from the path and used as the db name.

using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;

namespace TenantExample
{
    public class TenantDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
        
        private readonly IHttpContextAccessor _httpContextAccessor;

        public TenantDbContext(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var path = _httpContextAccessor.HttpContext.Request.Path.Value;
            var name = path.Split('/')[1];
            optionsBuilder.UseSqlite($"Data Source={name}.db");
        }
    }
}
}

Inside the controller, the Route must include the tenant id as the first part of the path. As the tenant id

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;

namespace TenantExample.Controllers
{
  [ApiController]
  public class ProductsController : ControllerBase
  {

    private readonly TenantDbContext _context;
        
    public ProductsController(TenantDbContext context)
    {
      _context = context;
    }

    [HttpGet]
    [Route("{tenant}/[controller]")]
    public IEnumerable<Product> Get()
    {
      return _context.Products.ToList();
    }

    [HttpGet]
    [Route("{tenant}/[controller]/{id}")]
    public Product Get(string id)
    {
      return _context.Products.Find(id);
    }
        
    [HttpGet]
    [Route("{tenant}/[controller]/[action]/{id}")]
    public Product Show(string id)
    {
      return _context.Products.Find(id);
    }

    [HttpPost]
    [Route("{tenant}/[controller]")]
    public Product Post([FromBody] Product product)
    {
      _context.Add(product);
      _context.SaveChanges();
      return product;
    }
    
  }
}

The last thing to do is to setup the Ingress with a rule that includes the tenant. The following rule has the tenant as the first part, the second part is the static name of the app the tenant wants to use and the rest is the resource.

Rule: host/<tenant>/app/<resource>

apiVersion: v1
kind: Service
metadata:
  name: tenantexample-service
spec:
  selector:
    app: tenantexample
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tenantexample
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: tenantexample
  template:
    metadata:
      labels:
        app: tenantexample
    spec:
      containers:
      - name: tenantexample
        image: localhost:32000/tenantexample:1.0.0
        ports:
        - containerPort: 80

---

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: tenantexample-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1/$2
spec:
  rules:
    - http:
        paths:
        - path: /?(.*)/app/?(.*)
          backend:
            serviceName: tenantexample-service
            servicePort: 80

comments powered by Disqus