Multi-tenant

MultiTenant

This core feature is useful to verify other core features that we have built because it is developed as a microservice. It’s integrated with Finbuckle to identity the tenant. We provide multiple choices for tenant data store:

  • File store
  • EFCore store
  • gRPC store

We also provide the gRPC API to manage tenant information and tenant settings, so MultiTenant itself is a service with the full implementation of:

  • MultiTenant DBContext
  • UnitOfWork pattern
  • Audit
  • gRPC API
  • DDD/CQRS and Domain Events pattern
  • Integration with event bus
  • Per-tenant configuration/options
  • xUnit helper

To use multi-tenant, we must register services with Tenant type

    var tenantBuilder = builder.Services
        // default Finbuckle services
        .AddMultiTenant<Tenant>(options =>
        {
            // configure tenant options
        })
        .WithBasePathStrategy(options => options.RebaseAspNetCorePathBase = true)
        // AND/OR other strategies
        ;

After that, we have 2 choices:

  1. Use multi-tenant as a separate serice by building a multi-tenant server and use gRPC client to get tenant information.

So we will register services for tenant host inside tenant microservice.

    ...
    using Juice.MultiTenant.Api.DependencyInjection;
    ...

    tenantBuilder.ConfigureTenantHost(builder.Configuration, options =>
        {
            options.DatabaseProvider = "PostgreSQL";
            options.ConnectionName = "PostgreConnection";
            options.Schema = "App";
        });

Then register services for tenant client inside other microservices.

    ...
    using Juice.MultiTenant.Grpc.Finbuckle.DependencyInjection;
    ...

    tenantBuilder.ConfigureTenantClient(builder.Configuration, builder.Environment.EnvironmentName);
  1. Use multi-tenant service directly.
  • Default EF store build
    ...
    using Juice.MultiTenant.EF.DependencyInjection;
    ...

    tenantBuilder.ConfigureTenantEFDirectly(builder.Configuration, options =>
        {
            options.DatabaseProvider = "PostgreSQL";
            options.ConnectionName = "PostgreConnection";
            options.Schema = "App";
        }, builder.Environment.EnvironmentName);
  • Or manually build tenant service with multiple store options.
    ...
    using Juice.MultiTenant.Finbuckle.DependencyInjection;
    ...

    tenantBuilder.JuiceIntegration()
        .WithGprcStore(tenantGrpcEndpoint)
        .WithDistributedCacheStore()
        // .WithEFStore(...)
        // you can add many stores at the same time
        ;

Per-tenant Authentication

We provide a custom WithPerTenantAuthenticationConventions extension beside the original WithPerTenantAuthenticationConventions from Finbuckle. It allows you to handle cross-tenant authorization yourself.

using Juice.MultiTenant.AspNetCore;
...
 services
    .AddMultiTenant()
    .WithBasePathStrategy(options => options.RebaseAspNetCorePathBase = true)
    .ConfigureTenantEFDirectly(configuration, options =>
    {
        options.DatabaseProvider = "PostgreSQL";
        options.ConnectionName = "TenantDbConnection";
        options.Schema = "App";
    }, environment.EnvironmentName)
    .WithPerTenantOptions<OpenIdConnectOptions>((options, tc) =>
    {
        options.Authority = authority + $"/{tc.Identifier}";
    })
    .WithPerTenantAuthenticationCore() // it's required
    .WithPerTenantAuthenticationConventions(crossTenantAuthorize: (authTenant, currentTenant, principal) =>
        authTenant == null // root tenant
        && (principal?.Identity?.IsAuthenticated ?? false) // authenticated
        && principal.IsInRole("admin"))
    .WithRemoteAuthenticationCallbackStrategy()
    ;

The library can be accessed via Nuget and npmjs:

MultiTenant DBContext

Please follow up on Data Isolation with Entity Framework Core to understand this section.

We usually have two options for storing per-tenant application data:

  • Separate DB with different DB connection strings, it can be done by per-tenant setting
  • Shared DB with an extra TenantId column to determine tenant data

In the second way, we will create a TenantDbContext that implement IMultiTenantDbContext interface. So we provide a MultiTenantDbContext abstraction class that implement these interfaces:

  • ISchemaDbContext
  • IAuditableDbContext
  • IUnitOfWork
  • IMultiTenantDbContext

You can inherit MultiTenantDbContext abstraction and mark your entity IsMultiTenant(), and/or IsAuditable().

    using Juice.EF.Extensions;
    using Juice.EF.MultiTenant;
    ...

    public class TenantContentDbContext : MultiTenantDbContext
    {
        public DbSet<TenantContent> TenantContents { get; set; }
        public TenantContentDbContext(IServiceProvider serviceProvider, DbContextOptions options) : base(options)
        {
            ConfigureServices(serviceProvider);
        }

        protected override void ConfigureModel(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TenantContent>(options =>
            {
                options.ToTable(nameof(TenantContent), Schema);
                options.IsMultiTenant();
                options.IsAuditable();
            });
        }
    }

Or using the [MultiTenant] attribute in entity class

    using Finbuckle.MultiTenant;
    [MultiTenant]
    public class TenantContent
    {
        ...
    }

See MultiTenantDbContext for more information.

The library can be accessed via Nuget: