Mellon.MultiTenant by @PubDev
Package | Version | Alpha |
---|---|---|
Mellon.MultiTenant | ||
Mellon.MultiTenant.Base | ||
Mellon.MultiTenant.ConfigServer | ||
Mellon.MultiTenant.Azure | ||
Mellon.MultiTenant.Hangfire |
Why Mellon, mellon is the Sindarin (and Noldorin) word for "friend", yes I'm a big fan of LoR, so let's be friends?
This library was created to supply a set of tools to enable the creation of multi-tenant applications using .net.
- net8
- steeltoe
- Azure App Configuration
- Spring Cloud Config
- The most important, Love ❤️
With package Manager:
Install-Package Mellon.MultiTenant
With .NET CLI:
dotnet add package Mellon.MultiTenant
There are two ways to configure the settings, via config and through the api
"MultiTenant": {
"ApplicationName": "customer-api",
"HttpHeaderKey": "x-tenant-name",
"CookieKey": "tenant-name",
"QueryStringKey": "tenant-name",
"TenantSource": "Settings",
"SkipTenantCheckPaths": ["^/swagger.*"],
"Tenants": [
"client-a",
"client-b",
"client-c"
]
}
Property | Description | Default |
---|---|---|
ApplicationName | Application name | IHostEnvironment.ApplicationName |
HttpHeaderKey | HTTP Header key, where the tenant name will be passed | null |
CookieKey | HTTP Cookie key, where the tenant name will be passed | null |
QueryStringKey | HTTP Query String key, where the tenant name will be passed | null |
TenantSource | Where the list of possible tenants will be stored, it can be from two sources: Settings or EnvironmentVariables |
Settings |
Tenants | When the property TenantSource is set to Settings this property must contain the list of tenants |
null |
WithDefaultTenant | When the tenant is not defined by the caller the lib will set the tenant as the tenant defined within this property, use it just when actually needed 😉👍 | null |
SkipTenantCheckPaths | Endpoints which the tenant do not need to be identified, for example: Swagger endpoints, you can use a regex string ^/swagger.* |
null |
When TenantSource
is set to EnvironmentVariables
it will get the tenant list from the environment variable MULTITENANT_TENANTS
, this environment variable must contain the list of possible tenants in a single string, separating the tenants using ,
for example:
$Env:MULTITENANT_TENANTS = 'client-a,client-b,client-c'
You can also set the settings using these options while you are adding the services
builder.Services
.AddMultiTenant(options =>
options
.WithApplicationName("customer-api")
.WithHttpHeader("x-tenant-name")
.WithCookie("tenant-name")
.WithQueryString("tenant-name")
.WithDefaultTenant("client-a")
.LoadFromSettings()
);
- Set the application name
- Set the HTTP Header key, where the tenant name will be passed
- Set the HTTP Cookie key, where the tenant name will be passed
- Set the HTTP Query String key, where the tenant name will be passed
- Set for when the tenant is not defined by the caller the lib will set the tenant as the tenant defined within this property, use it just when needed 😉👍
- Add a path that will be skipped during the tenant identification
- Add paths that will be skipped during the tenant identification
- Set for when the tenant list will be loaded from the settings MultiTenant:Tenants
- Set for when the tenant list will be loaded from the environment variable MULTITENANT_TENANTS
LoadFromEndpoint(Func<EndpointSettings, IConfiguration, string[]> func)
and LoadFromEndpoint<T>(Func<T, string> func)
- Define a Func or pass a type to define how the tenant list will be loaded from a http endpoint, to make it work you need to pass a new set of properties on the app settings
"MultiTenant": {
// other settings...
"Endpoint": {
"Url": "[endpoint]",
"Method": "GET",
"Authorization": "Basic $user $password"
},
// other settings...
}
If the endpoint has authorization you can set the credencials on the property Authorization
You can pass the Func and do what you see fit when getting the list of tenants
Example:
services
.AddMultiTenant(options => options.LoadFromEndpoint((endpointOptions, configuration) =>
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri(endpointOptions.Url),
Method = new HttpMethod(endpointOptions.Method),
};
if (!string.IsNullOrEmpty(endpointOptions.Authorization))
{
request.Headers.Add("Authorization", endpointOptions.Authorization);
}
using (var client = new HttpClient())
{
var result = client.Send(request);
if (result.IsSuccessStatusCode)
{
var data = result.Content.ReadFromJsonAsync<IEnumerable<Tenant>>().GetAwaiter().GetResult();
return data!.Select(x => x.Id).ToArray();
}
else
{
var statusCode = result.StatusCode;
var reason = result.ReasonPhrase;
var content = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
throw new Exception($@"Error to load tenants from the url {endpointOptions.Url} StatusCode: {statusCode} Reason: {reason} Content: {content}");
}
}
}));
Or if your use case does not require customization you can just call the other method, Which behind the scene does basically a http request to the endpoint set on the Endpoint
settings, respecting the Url
, Method
and Authorization
Example:
services
.AddMultiTenant(options => options.LoadFromEndpoint<Tenant>(x => x.TenantId));
- When all the possibilities above do not meet your need you can create a custom "Middleware" to identify the tenant based on a
HttpContext
T
must be an implementation of the interfaceITenantConfigurationSource
use it to define new a source of configurations for the tenants, for example, if the tenant settings are stored on XML files you could create something like this:
public class LocalXmlTenantSource : ITenantConfigurationSource
{
private readonly IHostEnvironment _hostEnvironment;
public LocalTenantSource(
IHostEnvironment hostEnvironment)
{
_hostEnvironment = hostEnvironment;
}
public IConfigurationBuilder AddSource(
string tenant,
IConfigurationBuilder builder)
{
builder.AddXmlFile($"appsettings.{tenant}.xml", true);
builder.AddXmlFile($"appsettings.{tenant}.{_hostEnvironment.EnvironmentName}.xml", true);
return builder;
}
}
This is the default source of settings for the tenants, there is no need to enable it, it will search for the settings on the application following this pattern:
appsettings.{tenant}.json
appsettings.{tenant}.{_hostEnvironment.EnvironmentName}.json
It is also worth mentioning that the configurations will also contain:
appsettings.json
appsettings.[environment].json
environment variables
You can also load the settings from a Spring Cloud Config Server!
To enable the usage you need to install an extra package:
With package Manager:
Install-Package Mellon.MultiTenant.ConfigServer
With .NET CLI:
dotnet add package Mellon.MultiTenant.ConfigServer
Once the package is installed you need to configure its services
builder.Services
.AddMultiTenant()
.AddMultiTenantSpringCloudConfig();
To setup Spring Cloud Config on your environment check this reporitory DotNet-ConfigServer
The application name for spring cloud config will be based on the settings MultiTenant.ApplicationName and the label will be tenant name.
Example: customer-api-client-a.yaml
being:
- customer-api the application name
- client-a the tenant name
Moreover, it is worth mentioning that the settings for each customer will also have the settings of the current files:
- appsettings.json
- appsettings.[environment].json
- environment variables
You can also use it as a source of configuration the Azure App Configuration
With package Manager:
Install-Package Mellon.MultiTenant.Azure
With .NET CLI:
dotnet add package Mellon.MultiTenant.Azure
Once the package is installed you need to configure its services
builder.Services
.AddMultiTenant()
.AddMultiTenantAzureAppConfiguration();
if the action is not passed, the connection string used to connect on azure will be loaded from AzureAppConfigurationConnectionString
if you want to elaborate more, on how you are going to connect on Azure, you can use the AzureMultiTenantOptions
, there is a property, which is a Func<IServiceProvider, string, Action<AzureAppConfigurationOptions>>
, where the first parameter is the ServiceProvider, where you can extract the services; a string, being the tenant name; and the return of this Func
must be an Action<AzureAppConfigurationOptions>
.
For example:
builder.Services
.AddMultiTenant()
.AddMultiTenantAzureAppConfiguration(options =>
options.AzureAppConfigurationOptions = (serviceProvider, tenant) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
return azureOptions => azureOptions
.Connect(configuration["AzureAppConfigurationConnectionString"])
.Select("*", tenant);
}
);
If you need to work with jobs with the concept of multi-tenant using Hangfire
To enable the usage you need to install an extra package:
With package Manager:
Install-Package Mellon.MultiTenant.Hangfire
With .NET CLI:
dotnet add package Mellon.MultiTenant.Hangfire
Once the package is installed you need to configure its services
builder.Services
.AddMultiTenant()
.AddMultiTenantHangfire();
And when adding the Service AddHangfire
you need to call the method UseMultiTenant
passing the IServiceProvider
builder.Services.AddHangfire((serviceProvider, config) =>
{
// some code
config.UseMultiTenant(serviceProvider);
// some code
});
To create the Worker, you need to pass the queues with the tenant names
builder.Services.AddHangfireServer((serviceProvider, config) =>
{
var multiTenantSettings = serviceProvider.GetRequiredService<MultiTenantSettings>();
var queues = new List<string>(multiTenantSettings.Tenants);
// if you want to add more queues
queues.Add("cron");
queues.Add("default");
config.Queues = tenants.ToArray();
// some code
});
For ScheduleJobs and BackgroundJob the queue name will be the tenant name
For RecurringJobs the default queue could vary from job to job
To create Recurring Jobs you just need to use the interface IMultiTenantRecurringJobManager
, this interface will have the following methods and extension methods:
AddOrUpdateForAllTenants<T>(string recurringJobId,Expression<Func<T, Task>> methodCall, string cronExpression, TimeZoneInfo timeZone = null,string queue = "default")
It will create a recurring job from the type T.Method
for all the tenants
AddOrUpdateForAllTenants(string recurringJobId, Job job, string cronExpression, TimeZoneInfo timeZone)
It will create a recurring job for all the tenants
AddOrUpdate<T>(string recurringJobId,Expression<Func<T, Task>> methodCall, string cronExpression, TimeZoneInfo timeZone = null,string queue = "default")
It will create a recurring job from the type T.Method
for the current tenant
It will create a recurring job for the current tenant
It will remove the recurring job for all the tenants
It will remove the recurring job for the current tenants
It will enqueue the recurring job for all the tenants
It will enqueue the recurring job for the current tenants
To create Background Jobs you just need to use the interface IMultiTenantBackgroundJobManager
, this interface will have the following methods and extension methods:
It will enqueue a job execution for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId created for that tenant
It will enqueue a job execution for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId created for that tenant
It will enqueue a job execution of type T.Method
for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId created for that tenant
It will enqueue a job execution of type T.Task<Method>
for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId created for that tenant
It will enqueue a job execution for the current tenant, sending the job for a queue with the tenant's name, the return the jobId
It will enqueue a job execution for the current tenant, sending the job for a queue with the tenant's name, the return the jobId
It will enqueue a job execution of type T.Method
for the current tenant, sending the job for a queue with the tenant's name, the return the jobId
It will enqueue a job execution of type T.Task<Method>
for the current tenant, sending the job for a queue with the tenant's name, the return the jobId
To Schedule Jobs you just need to use the interface IMultiTenantBackgroundJobManager
, this interface will have the following methods and extension methods:
It will Schedule a job execution for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId Scheduled for that tenant
It will Schedule a job execution for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId Scheduled for that tenant
It will Schedule a job execution of type T.Method
for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId Scheduled for that tenant
It will Schedule a job execution of type T.Task<Method>
for all the tenant, sending the jobs for a queue with the tenant's name, the return object with consist in a list containing the tenant and the JobId Scheduled for that tenant
It will Schedule a job execution for the current tenant, sending the job for a queue with the tenant's name, returning the Scheduled JobId
It will enqueue a job execution for the current tenant, sending the job for a queue with the tenant's name, returning the Scheduled JobId
It will enqueue a job execution of type T.Method
for the current tenant, sending the job for a queue with the tenant's name, returning the Scheduled JobId
It will enqueue a job execution of type T.Task<Method>
for the current tenant, sending the job for a queue with the tenant's name, returning the Scheduled JobId
You can find some examples of how to use this library in the folder /samples
with WebApi and Hangfire examples
To enable it on your api you first need to add the services:
builder.Services.AddMultiTenant();
then you need also to register the middleware used to identify the tenant based on the HttpContext
app.UseMultiTenant();
Once that is done you will be able to use the interface IMultiTenantConfiguration
, this interface will behave the same as the IConfiguration
interface, but contain only the current tenant settings:
Example:
app.MapGet("/", (IMultiTenantConfiguration configuration) =>
{
return new
{
Tenant = configuration.Tenant,
Message = configuration["Message"],
};
});
To use it with EF Core is quite simple, you need to use the interface IMultiTenantConfiguration
as mentioned above to setup your EF Context
builder.Services.AddDbContext<DataBaseContext>(
(IServiceProvider serviceProvider, DbContextOptionsBuilder options) =>
{
var configuration = serviceProvider.GetRequiredService<IMultiTenantConfiguration>();
options.UseSqlServer(configuration?["ConnectionStrings:DefaultConnection"]);
});
To apply the migrations, you only need to do this:
var tenants = app.Services.GetRequiredService<MultiTenantSettings>();
foreach (var tenant in tenants.Tenants)
{
using (var scope = app.Services.CreateScope())
{
var tenantSettings = scope.ServiceProvider.GetRequiredService<TenantSettings>();
tenantSettings.SetCurrentTenant(tenant);
var db = scope.ServiceProvider.GetRequiredService<DataBaseContext>();
await db.Database.MigrateAsync();
}
}
app.Run();
We know that settings can be changed all the time, but to get our applications running on with the latest settings we need to restart the application, it caused downtime and it's not very practical. Keeping that in mind, we added also an endpoint that when called will refresh all the settings for all the tenants or a specific tenant:
/refresh-settings
/refresh-settings/{tenantName}
PS: this will work only with AzureAppConfiguration and SpringCloudConfig
- Add unit tests 🧪
- Add new Config Source
- Load the Tenants from a web-api request
- Enable the usage with HangFire
- Update documentation with new features
See the open issues for a full list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
- Humberto Rodrigues - @1bberto - [email protected]
- Rafael Nagai - @naganaga - [email protected]
Project Page: https://pub-dev.github.io/Mellon.MultiTenant