c# Dotnettency Dotnettency is a library that provides features to enable Multi-Tenant applications using either:
- ASP.NET Core
- OWIN
Branch | AppVeyor | DevOps |
---|---|---|
Master | ||
Develop |
Package | Stable | Pre-release |
---|---|---|
Dotnettency Core | ||
AspNetCore | ||
Owin | ||
EF Core | ||
Tenant File System | ||
Autofac | ||
StructureMap | ||
Configuration |
Inspired by saaskit
- Tutorial series here: http://darrelltunnell.net/tutorial/creating-modular-multi-tenant-asp-net-core-application-with-dotnettency/
- Various samples here: https://github.com/dazinator/Dotnettency.Samples
- More extensive sample app for a full display of all the current features or if you want to see MVC in action, checkout the MVC sample
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.
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.
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.
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.
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 toawait
the currentTenant
(so non blocking).Task<TTenant>
is convenient. - Inject
ITenantAccessor<TTenant>
. This is similar to injectingTask<Tenant>
in that it provides lazy access the current tenant in a non blocking way. For convenience it's now easier to just injectTask<Tenant>
instead, unless you want a more descriptive API.
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>
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.
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 are special kind of item that have a lifetime tied to the current tenant, and are stored in the tenant's TenantShell
.
They are:
- Lazily initialised on first access, per tenant.
- Stored in the tenant's
TenantShell
for the lifetime of that tenant. - If the tenant is restarted, the value is cleared from the tenant's shell, and lazily re-initialised on next access again.
- If your
TItem
implementsIDisposable
it will be disposed of when the it is removed from theTenantShell
(typically on a tenant restart) accessed asynchronously. - It will be registered for DI as
Task<TItem>
so you can inject it and then await theTask
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>();
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");