/Dotnettency

Mutlitenancy for dotnet applications

Primary LanguageC#MIT LicenseMIT

c# Dotnettency Dotnettency is a library that provides features to enable Multi-Tenant applications using either:

  • ASP.NET Core
  • OWIN
Branch AppVeyor DevOps
Master Build status
Develop Build status Build Status
Package Stable Pre-release
Dotnettency Core Dotnettency Dotnettency
AspNetCore AspNetCore AspNetCore
Owin Owin Owin
EF Core EF Core EF Core
Tenant File System Tenant FileSystem Tenant FileSystem
Autofac Autofac Autofac
StructureMap StructureMap StructureMap
Configuration Dotnettency Dotnettency

Inspired by saaskit

Resources

Features

Tenant resolution

You can define how you want to identify the current tenant, i.e using a url scheme, cookie, or any of your custom logic. You can then access the current tenant through dependency injection in your app.

Tenant Middleware Pipelines

In your web application (OWIN or ASP.NET Core), when the web server recieves a request, it typically runs it through a single "middleware pipeline". Dotnettency allows you to have a lazily initialised "Tenant Middleware Pipeline" created for each distinct tenant. In the tenant specific middleware pipeline, you can choose to include middleware conditionally based on current tenant information. For example, for one tenant, you may use Facebook Authentication middleware, where as for another you might not want that middleware enabled.

Tenant Containers / Services

In ASP.NET Core applications (Dotnettency also allows you to achieve this in OWIN applications even though OWIN doesn't cater for this pattern out of the box), you configure a global set of services on startup for dependency injection purposes. At the start of a request, ASP.NET Core middleware creates a scoped version of those services to satisfy that request. Dotnettency goes a step further, by allowing you to register services for each specific tenant. Dotnettency middleware then provides an IServiceProvider scoped to the request for the current tenant. This means services that are typically injected into your classes during a request can now be tenant specific. This is useful if, for example, you want one tenant to use a different IPaymentProvider etc from another based on tenant settings etc.

Tenant File System

Notes: For more in depth details on what Per Tenant File System is, see the README on the sample.

Allows you to configure an IFileProvider that returns files from a virtual directory build for the current tenant. For example, tenant foo might want to access a file /bar.txt which exists for them, but when tenant bar tries to access /bar.txt it doesn't exist for them - because each tenant has it's own distinct virtual directory. Tenant virtual directories can overlap by sharing access to common directories / files.

Tenant Injection

Once configured in startup.cs you can resolve the current tenant in any one of the following ways:

  • Inject TTenant directly (may block whilst resolving current tenant).
  • Inject Task<TTenant> - Allows you to await the current Tenant (so non blocking). Task<TTenant> is convenient.
  • Inject ITenantAccessor<TTenant>. This is similar to injecting Task<Tenant> in that it provides lazy access the current tenant in a non blocking way. For convenience it's now easier to just inject Task<Tenant> instead, unless you want a more descriptive API.

Tenant Restart (New in v2.0.0)

You can Restart a tenant. This does not stop the web application, or interfere with other tenants. When you trigger a Restart of a tenant, it means the current tenants TenantShell (and all state, such as Services, MiddlewarePipeline etc) are disposed of. Once the Restart has finished, it means the next http request to that tenant will result in the tenant intialising again from scratch. This is useful for example, if you register services or middleware based on some settings, and you want to allow the settings to be changed for the tenant and therefore services middleware pipeline to be rebuilt based on latest config. It is also useful if you have a plugin based architecture, and you want to allow tenants to install plugins whilst the system is running.

  • Tenant Container will be re-built (if you are usijng tenant services the method you use to register services for the current tenant will be re-rexecuted.)
  • Tenant Middleware Pipeline will be re-built (if you are using tenant middleware pipeline, it will be rebuilt - you'll have a chance to include additional middlewares etc.)

For sample usage, see the Sample.AspNetCore30.RazorPages sample in this solution, in partcular the Pages/Gicrosoft/Index.cshtml page.

Injext ITenantShellRestarter<Tenant> and invoke the Restart() method:

    public class IndexModel : PageModel
    {

        public bool IsRestarted { get; set; }

        public void OnGet()
        {

        }

        public async Task OnPost([FromServices]ITenantShellRestarter<Tenant> restarter)
        {
            await restarter.Restart();
            IsRestarted = true;
            this.Redirect("/");
        }
    }

and corresponding razor page:


@page
@using Sample.Pages.T1
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1>Tenant Gicrosoft Razor Pages!</h1>

    <form method="post">
        @{
            if (!@Model.IsRestarted)
            {
                <button asp-page="Index">Restart Tenant</button>
            }
            else
            {
                <button disabled asp-page="Index">Restart Tenant</button>
                <p>Tenant has been restarted, the next request will result in Tenant Container being rebuilt, and tenant middleware pipeline being re-initialised.</p>
            }
        }

    </form>
</div>

Tenant Shell Injection

The TenantShell stores the context for a Tenant, such as it's Container and it's MiddlewarePipeline. It's stored in a cache, and is evicted if the tenant is Restarted. You probably won't need to use it directly, but if you want you can do so.

  • Inject ITenantShellAccessor<TTenant> to access the TenantShell for the current tenant.
  • Extensions (such as Middleware, or Container) - store things for the tenant in it's concurrent property bag. You can get at these properties if you know the keys.
  • You can also register callbacks that will be invoked when the TenantShell is disposed of - this happens when the tenant is restarted for example.

Another way to register code that will run when the tenant is restarted, is to use TenantServices - add a disposable singleton service the tenant's container. When the tenant is disposed of, it's container will be disposed of, and your disposable service will be disposed of - depending upon your needs this hook might suffice.

Tenant IConfiguration (New in v2.1.0)

ASP.NET Core hosting model allows you to build an IConfiguration for your application settings. Dotnettency takes this further, by allowing each tenant to have it's own IConfiguration lazily constructed when the tenant is initialised (first request to the tenant). You can access the current tenant's IConfiguration by injecting Task<IConfiguration> into your Controllers. The snippet below shows how to configure tenant specific configuration, notice how it uses the current tenant's name to find the JSON file:

 .ConfigureTenantConfiguration((a) =>
                        {
                            var tenantConfig = new ConfigurationBuilder();
                            tenantConfig.AddJsonFile(Environment.ContentRootFileProvider, $"/appsettings.{a.Tenant?.Name}.json", true, true);
                            return tenantConfig;
                        })
						

You can now inject Task<IConfiguration> into your controllers etc, and await the result to obtain the tenants IConfiguration. Note: if you inject IConfiguration rather than Task<IConfiguration> you will get the usual application wide IConfiguration like normal (currently).

You can access the Tenant's `IConfiguration' when building the Tenant's middleware pipeline, or Container - this is designed such that you could use tenant specific configuration to decide how to configure that tenants middleware or services.

Tenant Shell Items

Tenant Shell Items are special kind of item that have a lifetime tied to the current tenant, and are stored in the tenant's TenantShell. They are:

  1. Lazily initialised on first access, per tenant.
  2. Stored in the tenant's TenantShell for the lifetime of that tenant.
  3. If the tenant is restarted, the value is cleared from the tenant's shell, and lazily re-initialised on next access again.
  4. If your TItem implements IDisposable it will be disposed of when the it is removed from the TenantShell (typically on a tenant restart) accessed asynchronously.
  5. It will be registered for DI as Task<TItem> so you can inject it and then await the Task to get the value. The await is necessary as the value will be asynchronously created only on first access for that tenant. On subsequent accesses, the same cached task (already completed) is used to return the value immediately.

You can register a tenant shell item during dotnettency fluent configuration like:

             services.AddMultiTenancy<Tenant>((builder) =>
             {
                 builder.IdentifyTenantsWithRequestAuthorityUri()
                         // .. shortened for brevity
                        .ConfigureTenantShellItem((tenantInfo) =>
                        {
                            return new ExampleShellItem { TenantName = tenantInfo.Tenant?.Name };
                        })

You can now access this through DI:


public class MyController
{
      public MyController(Task<ExampleShellItem> shellItem)
	  {
	  
	  }

}

Note: If you don't like injecting Task<T> you can also inject ITenantShellItemAccessor<TTenant, TItem> and use that to get access to the shell item

You can also access the shell item during most fluent configuration of a tenant, for example most fluent configuration methods expose a context object with a GetShellItemAsync extension method:

 var exampleShellItem = await context.GetShellItemAsync<ExampleShellItem>();

Named Shell Items?

Suppose you want to register multiple of your Shell Item instances, with different names. You can use the ConfigureNamedTenantShellItems instead.

             services.AddMultiTenancy<Tenant>((builder) =>
             {
                 builder.IdentifyTenantsWithRequestAuthorityUri()
                         // .. shortened for brevity
                         .ConfigureNamedTenantShellItems<Tenant, ExampleShellItem>((b) =>
                         {
                             b.Add("red", (c) => new ExampleShellItem(c.Tenant?.Name ?? "NULL TENANT") { Colour = "red" });
                             b.Add("blue", (c) => new ExampleShellItem(c.Tenant?.Name ?? "NULL TENANT") { Colour = "blue" });
                         });


To access a named shell item through DI, rather than injecting Task<T>, you can inject Func<string, Task`:

public class MyController
{
      private readonly Func<string, Task<T>> _namedShellItemFactory
     
	  public MyController(Func<string, Task<T>> namedShellItemFactory)
	  {
	     _namedShellItemFactory = namedShellItemFactory;
	  }

	  public async Task<T> GetRedItem()
	  {
	      return await _namedShellItemFactory("red");
	  }

}

Or during fluent configuration of the tenant, for exmaple whilst configuring the tenant's middleware pipeline or services:

 var redShellItem = await context.GetShellItemAsync<ExampleShellItem>("red");