Mellon-MultiTenant-ConfigServer
1.1.48-alpha
See the version list below for details.
dotnet add package Mellon-MultiTenant-ConfigServer --version 1.1.48-alpha
NuGet\Install-Package Mellon-MultiTenant-ConfigServer -Version 1.1.48-alpha
<PackageReference Include="Mellon-MultiTenant-ConfigServer" Version="1.1.48-alpha" />
paket add Mellon-MultiTenant-ConfigServer --version 1.1.48-alpha
#r "nuget: Mellon-MultiTenant-ConfigServer, 1.1.48-alpha"
// Install Mellon-MultiTenant-ConfigServer as a Cake Addin #addin nuget:?package=Mellon-MultiTenant-ConfigServer&version=1.1.48-alpha&prerelease // Install Mellon-MultiTenant-ConfigServer as a Cake Tool #tool nuget:?package=Mellon-MultiTenant-ConfigServer&version=1.1.48-alpha&prerelease
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?
About The Project
This library was created to supply a set of tools to enable the creation of multi-tenant applications using .net.
Built With
- net7
- steeltoe
- Azure App Configuration
- Spring Cloud Config
- The most important, Love โค๏ธ
Getting Started
Installation
With package Manager:
Install-Package Mellon.MultiTenant
With .NET CLI:
dotnet add package Mellon.MultiTenant
Configurations
There are two ways to configure the settings, via config and through the api
Settings
"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'
Using the API
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()
);
WithApplicationName(string)
- Set the application name
WithHttpHeader(string)
- Set the HTTP Header key, where the tenant name will be passed
WithCookie(string)
- Set the HTTP Cookie key, where the tenant name will be passed
WithQueryString(string)
- Set the HTTP Query String key, where the tenant name will be passed
WithDefaultTenant(string)
- 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 ๐๐
WithSkipTenantCheckPaths(string)
- Add a path that will be skipped during the tenant identification
WithSkipTenantCheckPaths(params string[])
- Add paths that will be skipped during the tenant identification
LoadFromSettings
- Set for when the tenant list will be loaded from the settings MultiTenant:Tenants
LoadFromEnvironmentVariable
- 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));
WithHttpContextLoad(Func<HttpContext, string> func)
- When all the possibilities above do not meet your need you can create a custom "Middleware" to identify the tenant based on a
HttpContext
WithCustomTenantConfigurationSource<T>()
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;
}
}
Local
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
Spring Cloud Config
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
Azure App Configuration
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();
AddMultiTenantAzureAppConfiguration(Action<AzureMultiTenantOptions> action = null)
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);
}
);
Hangfire
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
RecurringJobs ๐๏ธ
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
AddOrUpdate(string recurringJobId, Job job, string cronExpression, TimeZoneInfo timeZone)
It will create a recurring job for the current tenant
RemoveIfExistsForAllTenants(string recurringJobId)
It will remove the recurring job for all the tenants
RemoveIfExists(string recurringJobId)
It will remove the recurring job for the current tenants
TriggerForAllTenants(string recurringJobId)
It will enqueue the recurring job for all the tenants
Trigger(string recurringJobId)
It will enqueue the recurring job for the current tenants
Background Jobs โ๏ธ
To create Background Jobs you just need to use the interface IMultiTenantBackgroundJobManager
, this interface will have the following methods and extension methods:
EnqueueForAllTenants(Expression<Action> methodCall)
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
EnqueueForAllTenants(Expression<Func<Task>> methodCall)
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
EnqueueForAllTenants<T>(Expression<Action<T>> methodCall)
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
EnqueueForAllTenants<T>(Expression<Func<T, Task>> methodCall)
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
Enqueue(Expression<Action> methodCall)
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
Enqueue(Expression<Func<Task>> methodCall)
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
Enqueue<T>(Expression<Action<T>> methodCall)
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
Enqueue<T>(Expression<Func<T, Task>> methodCall)
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
Schedule Jobs โ
To Schedule Jobs you just need to use the interface IMultiTenantBackgroundJobManager
, this interface will have the following methods and extension methods:
ScheduleForAllTenants(Expression<Action> methodCall, TimeSpan delay)
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
ScheduleForAllTenants((Expression<Func<Task>> methodCall, TimeSpan delay)
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
ScheduleForAllTenants<T>(Expression<Action<T>> methodCall, TimeSpan delay)
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
ScheduleForAllTenants<T>(Expression<Func<T, Task>> methodCall, TimeSpan delay)
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
Schedule(Expression<Action> methodCall, TimeSpan delay)
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
Schedule(Expression<Func<Task>> methodCall, TimeSpan delay)
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
Schedule<T>(Expression<Action<T>> methodCall, TimeSpan delay)
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
Schedule<T>(Expression<Func<T, Task>> methodCall, TimeSpan delay)
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
Usage / Samples
You can find some examples of how to use this library in the folder /samples
with WebApi and Hangfire examples
Web API
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"],
};
});
EF Core Migrations:
To use it with EF Core is quite simple, you need to use the interface IMultiTenantConfiguration
as mentioned above to setup your EF Context
Setup
builder.Services.AddDbContext<DataBaseContext>(
(IServiceProvider serviceProvider, DbContextOptionsBuilder options) =>
{
var configuration = serviceProvider.GetRequiredService<IMultiTenantConfiguration>();
options.UseSqlServer(configuration?["ConnectionStrings:DefaultConnection"]);
});
Migrations
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();
Extras
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
Roadmap
- 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).
Contributing
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
Contact
- Humberto Rodrigues - @1bberto - humberto_henrique1@live.com
- Rafael Nagai - @naganaga - rafakenji23@gmail.com
Project Page: https://pub-dev.github.io/Mellon.MultiTenant
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net7.0
- Mellon-MultiTenant-Base (>= 1.1.48-alpha)
- Steeltoe.Extensions.Configuration.ConfigServerCore (>= 3.2.4)
- Steeltoe.Extensions.Configuration.PlaceholderCore (>= 3.2.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
3.0.51-alpha | 69 | 10/2/2024 |
3.0.36 | 85 | 10/2/2024 |
2.0.49-alpha | 207 | 11/26/2023 |
2.0.35 | 248 | 11/26/2023 |
1.1.48-alpha | 144 | 10/23/2023 |
1.1.46-alpha | 151 | 8/1/2023 |
1.1.45-alpha | 156 | 7/8/2023 |
1.1.44-alpha | 140 | 6/20/2023 |
1.1.43-alpha | 150 | 4/4/2023 |
1.1.42-alpha | 138 | 4/4/2023 |
1.1.41-alpha | 159 | 3/25/2023 |
1.1.34 | 157 | 10/23/2023 |
1.1.33 | 180 | 8/1/2023 |
1.1.32 | 152 | 7/8/2023 |
1.1.31 | 162 | 6/20/2023 |
1.1.30 | 208 | 4/4/2023 |
1.1.29 | 245 | 3/25/2023 |
1.0.40-alpha | 153 | 2/25/2023 |
1.0.39-alpha | 166 | 2/25/2023 |
1.0.38-alpha | 149 | 2/25/2023 |
1.0.37-alpha | 153 | 2/25/2023 |
1.0.36-alpha | 157 | 2/24/2023 |
1.0.34-alpha | 150 | 2/17/2023 |
1.0.33-alpha | 166 | 1/6/2023 |
1.0.32-alpha | 159 | 1/6/2023 |
1.0.31-alpha | 170 | 1/4/2023 |
1.0.30-alpha | 168 | 1/4/2023 |
1.0.29-alpha | 171 | 1/4/2023 |
1.0.28 | 279 | 2/25/2023 |
1.0.28-alpha | 157 | 1/4/2023 |
1.0.27 | 260 | 2/25/2023 |
1.0.27-alpha | 143 | 1/2/2023 |
1.0.26 | 244 | 2/24/2023 |
1.0.26-alpha | 151 | 1/2/2023 |
1.0.25 | 247 | 2/24/2023 |
1.0.25-alpha | 164 | 1/2/2023 |
1.0.24 | 243 | 2/17/2023 |
1.0.24-alpha | 149 | 1/2/2023 |
1.0.23 | 311 | 1/6/2023 |
1.0.23-alpha | 178 | 1/2/2023 |
1.0.22 | 301 | 1/4/2023 |
1.0.22-alpha | 162 | 1/2/2023 |
1.0.21 | 292 | 1/3/2023 |
1.0.21-alpha | 146 | 1/2/2023 |
1.0.20 | 303 | 1/3/2023 |
1.0.20-alpha | 164 | 1/2/2023 |
1.0.19 | 298 | 1/3/2023 |
1.0.19-alpha | 175 | 1/2/2023 |
1.0.18 | 321 | 1/2/2023 |
1.0.18-alpha | 167 | 1/2/2023 |
1.0.17 | 310 | 1/2/2023 |
1.0.17-alpha | 163 | 1/2/2023 |
1.0.16 | 313 | 1/2/2023 |
1.0.16-alpha | 161 | 1/2/2023 |
1.0.15 | 307 | 1/2/2023 |
1.0.15-alpha | 171 | 1/2/2023 |
1.0.14 | 324 | 1/2/2023 |
1.0.14-alpha | 166 | 1/2/2023 |
1.0.13 | 303 | 12/30/2022 |
1.0.13-alpha | 162 | 1/2/2023 |
1.0.12-alpha | 171 | 1/2/2023 |
1.0.11-alpha | 147 | 12/30/2022 |
1.0.10-alpha | 153 | 12/30/2022 |