myquay / MultiTenant.AspNetCore

Multi-tenancy support for ASP.NET Core 8
MIT License
16 stars 4 forks source link

Project Logo MultiTenant.AspNetCore

Multi-tenancy support for ASP.NET Core 8

About

A lightweight, easy to configure, open-source library which allows you to build multi-tenanted applications in ASP.NET Core 8.

It supports

A deep-dive on the library internals is available here: https://michael-mckenna.com/multi-tenant-asp-dot-net-8-tenant-resolution/

Quickstart

The library is designed to follow common ASP.NET Core patterns for ease of configuration.

Installation

The library is distributed as a NuGet package: https://www.nuget.org/packages/MultiTenant.AspNetCore/ you can install it using your favourite package manager, or download the source and compile it locally.

Define how your application manages tenants

Multi-tenant requirements vary widely by use-case, this library provides the following extension points to cater for a wide range of usecases

Tenant data (ITenantInfo)

Implement the ITenantInfo interface to define your tenant specific data.

public class TenantInfo : ITenantInfo
{
  public required string Id { get; set; }
  public required string Name { get; set; }
  ... other properties ...
}

Tenant identification (ITenantResolutionStrategy)

Implement the ITenantResolutionStrategy to define how tenants are identified in your system, a common pattern is to give each tenant a different subdomain making the host a good candidate as an identifier. Here is an exmaple of how to implement a resolution strategy based on hostname.

public class HostResolutionStrategy(IHttpContextAccessor httpContextAccessor) : ITenantResolutionStrategy
{
    private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;

    public async Task<string> GetTenantIdentifierAsync()
    {
        if (_httpContextAccessor.HttpContext == null)
            throw new InvalidOperationException("HttpContext is not available");

        return await Task.FromResult(_httpContextAccessor.HttpContext.Request.Host.Host);
    }
}

Tenant lookup (ITenantLookupService<>)

Implement the ITenantLookupService<TenantInfo> interface to define how your application loads tenant configuration. This could be from memory, configuration, a database, or other durable data store depending on your requirements.

The lookup service accepts the identifier returned from your tenant resolution strategy to find the tenant.

 public class TenantLookupService() : ITenantLookupService<TenantInfo>
 {
     public Task<TenantInfo> GetTenantAsync(string identifier)
     {
         ... your implementation ...
     }
 }

Basic configuration

Configure your application to support multi-tenancy in the same place you register all your middleware and services.

//Add the library to your application and define the tenant data available in your tenant context 
builder.Services.AddMultiTenancy<TenantInfo>()
    //Specify your resolution strategy
    .WithResolutionStrategy<HostResolutionStrategy>()
    //Specify your tenant data provider 
    .WithTenantLookupService<TenantLookupService>();

You're done, whenever you want to access the current tenant just inject IMultiTenantContextAccessor<TenantInfo> using ASP.NET Core DI and you'll have access to the current tenant.

Advanced configuration

Per-tenant services & options

The library supports configuring services or options differently for different tenants, this allows you to do things such as register a database context with a seperate connection string etc.

    ///Add a service configured different per-tenant
    .WithTenantedServices((services, tenant) =>
    {
       if (tenant != null)
           services.AddSingleton<SomeService>(options =>
           {
               options.SomeSetting = tenant.SomeTenantSpecificSetting;
           });
    })
    ///Register different options per-tenant (e.g. different localisations)
    .WithTenantedConfigure<RequestLocalizationOptions>((options, tenant) =>
    {
        var supportedCultures = tenant?.CultureOptions ?? ["en-NZ"];
        options.SetDefaultCulture(supportedCultures[0])
            .AddSupportedCultures(supportedCultures)
            .AddSupportedUICultures(supportedCultures);

    });

Per-tenant pipeline

The library also supports modifying the middleware pipeline based on tenant. This allows different tenants to load completely different middleware and can be especially useful if a middleware captures its configuration on startup.

For example the localisation middleware caches its configuration on start-up so by the time a request comes in we cannot alter it for that tenant's configuration. By having a tenant specific pipeline the configuration options captured by the middleware on start-up is now tenant specific.

In the example below, the application will now respect the localisation of each tenant even though the middleware does not allow configuration to change after startup.

app.UseMultiTenantPipeline<TenantOptions>((tenant, app) =>
{
    app.UseRequestLocalization();
});

This makes our library compatible with a wide range of existing middleware without the need of additional work-arounds.

Disable automatic tenant resolution middleware registration

By default the library will attempt to resolve the current tenant at the start of the middleware pipeline so that the tenant context is available as early as possible.

However, if your tenant resolution strategy cannot resolve a tenant identifier this early on you can disable this behvaior and manually specify when tenant resolution should be attempted using app.UseMultiTenancy<TenantInfo>()

First disable automatic registration

services.AddMultiTenancy<TestTenant>(o => { o.DisableAutomaticPipelineRegistration = true; })

The specify where the tenant resolution should be attempted

app.UseThis();
app.UseMultiTenancy<TestTenant>(); //Tenant resolution strategy used here
app.UseThat();

Roadmap

Contributing

Contributions are very welcome!

Ways to contribute

License

MIT