Finbuckle.
Finbuckle.MultiTenant is designed to emphasize using per-tenant options in an app to drive per-tenant behavior. This approach allows app logic to be written having to add tenant-dependent or tenant-specific logic to the code.
By using per-tenant options, the options values used within app logic will automatically reflect the per-tenant values as configured for the current tenant. Any code already using the Options pattern will gain multi-tenant capability with minimal code changes.
Finbuckle.MultiTenant integrates with the standard .NET Options pattern (see also the ASP.NET Core Options pattern) and lets apps customize options distinctly for each tenant.
Note: For authentication options, Finbuckle.MultiTenant provides special support for per-tenant authentication.
The current tenant determines which options are retrieved via
the IOptions<TOptions>
, IOptionsSnapshot<TOptions>
, or IOptionsMonitor<TOptions>
instances' Value
property and
Get(string name)
method.
Per-tenant options will work with any options class when using IOptions<TOptions>
, IOptionsSnapshot<TOptions>
,
or IOptionsMonitor<TOptions>
with dependency injection or service resolution. This includes an app's own code and
code internal to ASP.NET Core or other libraries that use the Options pattern.
A potential issue arises when code internally stores or caches options values from
an IOptions<TOptions>
, IOptionsSnapshot<TOptions>
, or IOptionsMonitor<TOptions>
instance. This is usually
unnecessary because the options are already cached within the .NET options infrastructure, and in these cases the
initial instance of the options is always used, regardless of the current tenant. Finbuckle.MultiTenant works around
this for some parts of
ASP.NET Core, and recommends that in your own code to always access options values via
the IOptions<TOptions>
, IOptionsSnapshot<TOptions>
, or IOptionsMonitor<TOptions>
instance. This will ensure the
correct values for the current tenant are used.
Consider a typical scenario in ASP.Net Core, starting with a simple class:
public class MyOptions
{
public int Option1 { get; set; }
public int Option2 { get; set; }
}
In the app configuration, services.Configure<MyOptions>
is called with a delegate
or IConfiguration
parameter to set the option values:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyOptions>(options => options.Option1 = 1);
// ...rest of app code
Dependency injection of IOptions<MyOptions>
or its siblings into a class constructor, such as a controller, provides
access to the options values. A service provider instance can also provide access to the options values.
// access options via dependency injection in a class constructor
public MyController : Controller
{
private readonly MyOptions _myOptions;
public MyController(IOptionsMonitor<MyOptions> optionsAccessor)
{
// same options regardless of the current tenant
_myOptions = optionsAccessor.Value;
}
}
// or with a service provider
httpContext.RequestServices.GetServices<IOptionsSnaption<MyOptions>();
With standard options each tenant would get see the same exact options.
This sections assumes a standard web application builder is configured and Finbuckle.MultiTenant is configured with
a TTenantInfo
type of TenantInfo
.
See Getting Started for details.
To configure options per tenant, the standard Configure
method variants on the service collection now all
have PerTenant
equivalents which accept a Action<TOptions, TTenantInfo>
delegate. When the options are created at
runtime the delegate will be called with the current tenant details.
// configure options per tenant
builder.Services.ConfigurePerTenant<MyOptions, TenantInfo>((options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
// or configure named options per tenant
builder.Services.ConfigurePerTenant<MyOptions, TenantInfo>("scheme2", (options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
// ConfigureAll options variant
builder.Services.ConfigureAllPerTenant<MyOptions, TenantInfo>((options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
// can also configure post options, named post options, and all post options variants
builder.Services.PostConfigurePerTenant<MyOptions, TenantInfo>((options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
builder.Services.PostConfigurePerTenant<MyOptions, TenantInfo>("scheme2", (options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
builder.Services.PostConfigureAllPerTenant<MyOptions, TenantInfo>((options, tenantInfo) =>
{
options.MyOption1 = tenantInfo.Option1Value;
options.MyOption2 = tenantInfo.Option2Value;
});
Now with the same controller example from above, the option values will be specific to the current tenant:
public MyController : Controller
{
private readonly MyOptions _myOptions;
public MyController(IOptionsMonitor<MyOptions> optionsAccessor)
{
// _myOptions.MyOptions1 and .MyOptions2 will be specific to the current tenant.
_myOptions = optionsAccessor.Value;
}
}
.NET provides
the OptionsBuilder
API to provide more flexibility for configuring options. This pattern simplifies dependency injection and validation for
the standard Options pattern. Finbuckle.MultiTenant
extends this API to enable options configuration for per-tenant options similarly. Note that while the OptionsBuilder
normally supports up to five dependencies, Finbuckle.MultiTenant support only supports four.
// use OptionsBuilder API to configure per-tenant options with dependencies
builder.Services.AddOptions<MyOptions>("optionalName")
.ConfigurePerTenant<ExampleService, TenantInfo>(
(options, es, tenantInfo) =>
options.Property = DoSomethingWith(es, tenantInfo));
Internally .NET caches options, and Finbuckle.MultiTenant extends this to cache options per tenant. Caching
occurs when a TOptions
instance is retrieved via Value
or Get
on the injected IOptions<TOptions>
(or derived)
instance for the first time for a tenant.
IOptions<TOptions>
instances are always regenerated when injected so any caching only lasts as long as the specific
instance.
IOptionsSnapshot<TOptions>
instances are generated once per HTTP request and caching will last throughout the entire
request.
IOptionsMonitor<TOptions>
instances persist across HTTP requests and caching can persist for long periods of time.
In some situations cached options may need to be cleared so that the options can be regenerated.
When using per-tenant options via IOptions<TOptions>
and IOptionsSnapshot<TOptions>
the injected instance is of
type MultiTenantOptionsManager<TOptions>
. Casting to this type exposes the Reset()
method which clears any internal
caching for the current tenant and cause the options to be regenerated when next accessed via Value
or Get(string name)
.
When using per-tenant options with IOptionsMonitor<TOptions>
each injected instance uses a shared persistent cache.
This cache can be retrieved by injecting or resolving an instance of IOptionsMonitorCache<TOptions>
which has
a Clear()
method that will clear the cache for the current tenant. Casting the IOptionsMonitorCache<TOptions>
instance to MultiTenantOptionsCache<TOptions>
exposes the Clear(string tenantId)
and ClearAll()
methods. Clear(string tenantId)
clears cached options for a specific tenant (or the regular non per-tenant options if
the parameter is empty or null). ClearAll()
clears all cached options (including regular non per-tenant options).