Multi-tenant ASP.NET Core 2 - Implementing database based tenant provider

gpeipman
51.6K views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Database based tenant provider

This example proposes implementation of tenant provider that uses tenants database to detect current tenant. The idea is simple - information about existing tenants is held in some database and there is database context for this database. Tenant provider uses this context to find correct tenant for current request.

Tenants database

Let's define simple tenant entity first.

public class Tenant
{
    [Key]
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string HostName { get; set; }
}

We also need database context for tenants database.

public class TenantsDbContext : DbContext
{
    public DbSet<Tenant> Tenants { get; set; }

    public TenantsDbContext(DbContextOptions<TenantsDbContext> options) : base(options)
    {
    }

    public void AddSampleData()
    {
        Tenants.Add(new Tenant
        {
            Id = MultitenantDbContext.Tenant1Id,
            Name = "Imaginary corp.",
            HostName = "imaginary.example.com"
        });

        Tenants.Add(new Tenant
        {
            Id = MultitenantDbContext.Tenant2Id,
            Name = "The Very Big corp.",
            HostName = "big.example.com"
        });

        SaveChanges();
    }
}

Tenant provider

We start with defining tenant providers interface.

public interface ITenantProvider
{
    Guid GetTenantId();
}

We need also tenant provider that queries tenants database by current request host header.

public class DatabaseTenantProvider : ITenantProvider
{
    private Guid _tenantId;
        
    public DatabaseTenantProvider(TenantsDbContext context, IHttpContextAccessor accessor)
    {
        var host = accessor.HttpContext.Request.Host.Value;

        context.AddSampleData();

        // This is for real life cases
        //_tenantId = context.Tenants.First(t => t.HostName == host).Id;

        _tenantId = context.Tenants.First(t => t.HostName == "imaginary.example.com").Id;
    }

    public Guid GetTenantId()
    {
        return _tenantId;
    }
}

Multi-tenant database

For multi-tenant database we use simple Person entity.

public class Person
{
    [Key]
    public Guid Id { get; set; }
    public Guid TenantId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Database context that uses multi-tenant database uses tenant provider to set global query filter that makes sure that only enitites for current tenant are returned by queries.

public class MultitenantDbContext : DbContext
{
	public static Guid Tenant1Id = Guid.Parse("51aab199-1482-4f0d-8ff1-5ca0e7bc525a");
	public static Guid Tenant2Id = Guid.Parse("ae4e21fa-57cb-4733-b971-fdd14c4c667e");

	public DbSet<Person> People { get; set; }

	private ITenantProvider _tenantProvider;

	public MultitenantDbContext(DbContextOptions<MultitenantDbContext> options,
								ITenantProvider tenantProvider) : base(options)
        {
		_tenantProvider = tenantProvider;
	}

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		base.OnModelCreating(modelBuilder);

		modelBuilder.Entity<Person>().HasQueryFilter(p => p.TenantId == _tenantProvider.GetTenantId());
	}

	public void AddSampleData()
	{
		People.Add(new Person {
			Id = Guid.Parse("79865406-e01b-422f-bd09-92e116a0664a"),
			TenantId = Tenant1Id,
			FirstName = "Gunnar",
			LastName = "Peipman"
		});

		People.Add(new Person
		{
			Id = Guid.Parse("d5674750-7f6b-43b9-b91b-d27b7ac13572"),
			TenantId = Tenant2Id,
			FirstName = "John",
			LastName = "Doe"
		});

		People.Add(new Person
		{
			Id = Guid.Parse("e41446f9-c779-4ff6-b3e5-752a3dad97bb"),
			TenantId = Tenant1Id,
			FirstName = "Mary",
			LastName = "Jones"
		});

		SaveChanges();
	}
}

Registering services

In application start-up class database contexts and other things we need through dependency injection must be registered.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TenantsDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));
    services.AddDbContext<MultitenantDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));

    services.AddMvc();

    services.AddTransient<ITenantProvider, DatabaseTenantProvider>();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

Demo

Now everything is ready on application side and it's time to see the demo.

Click to run the .NET Core Web app.

References

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content