├── logo.png ├── samples ├── Api │ ├── Migrations │ │ ├── .editorconfig │ │ ├── 20230102162741_InitialMigration.cs │ │ ├── DataBaseContextModelSnapshot.cs │ │ └── 20230102162741_InitialMigration.Designer.cs │ ├── appsettings.client-a.json │ ├── appsettings.client-b.json │ ├── appsettings.client-c.json │ ├── Entities │ │ └── Product.cs │ ├── DataBaseContext.cs │ ├── Properties │ │ └── launchSettings.json │ ├── docker-compose.yml │ ├── appsettings.json │ ├── WebApi.csproj │ └── Program.cs └── Hangfire │ ├── appsettings.client-a.json │ ├── appsettings.client-b.json │ ├── appsettings.client-c.json │ ├── Properties │ └── launchSettings.json │ ├── Jobs │ ├── IEmailSender.cs │ └── EmailSender.cs │ ├── docker-compose.yml │ ├── appsettings.json │ ├── WebApiHangfire.csproj │ └── Program.cs ├── src ├── Mellon.MultiTenant.Base │ ├── Enums │ │ └── TenantSource.cs │ ├── Exceptions │ │ ├── TenantNotFoundException.cs │ │ └── TenantSourceNotSetException.cs │ ├── EndpointSettings.cs │ ├── Interfaces │ │ ├── IMultiTenantConfiguration.cs │ │ └── IMultiTenantConfigurationSource.cs │ ├── TenantSettings.cs │ ├── TenantConfiguration.cs │ ├── Mellon.MultiTenant.Base.csproj │ ├── MultiTenantSettings.cs │ ├── TypeExtensions.cs │ └── MultiTenantOptions.cs ├── Mellon.MultiTenant.Azure │ ├── AzureMultiTenantOptions.cs │ ├── AzureTenantSource.cs │ ├── Extensions │ │ └── MultiTenantExtensions.cs │ └── Mellon.MultiTenant.Azure.csproj ├── Mellon.MultiTenant.Hangfire │ ├── Interfaces │ │ ├── IMultiTenantRecurringJobManager.cs │ │ └── IMultiTenantBackgroundJobManager.cs │ ├── JobActivators │ │ └── MultiTenantHangfireJobActivator.cs │ ├── Attributes │ │ └── PreventConcurrentExecutionJobFilterAttribute.cs │ ├── Extensions │ │ ├── MultiTenantExtensions.cs │ │ └── MultiTenantRecurringJobManagerExtensions.cs │ ├── Mellon.MultiTenant.Hangfire.csproj │ ├── JobManagers │ │ ├── MultiTenantRecurringJobManager.cs │ │ └── MultiTenantBackgroundJobManager.cs │ └── Filters │ │ └── MultiTenantClientFilter.cs ├── Mellon.MultiTenant │ ├── LocalMultiTenantSource.cs │ ├── Extensions │ │ ├── HttpContextExtensions.cs │ │ └── MultiTenantExtensions.cs │ ├── Mellon.MultiTenant.csproj │ └── Middlewares │ │ └── HttpTenantIdentifierMiddleware.cs └── Mellon.MultiTenant.ConfigServer │ ├── Extensions │ └── MultiTenantExtensions.cs │ ├── ConfigServerMultiTenantSource.cs │ └── Mellon.MultiTenant.ConfigServer.csproj ├── Directory.Build.props ├── LICENSE ├── VersionConfig.yml ├── .github └── workflows │ ├── jekyll-gh-pages.yml │ ├── buildAndPush.yml │ └── buildAndPushPR.yml ├── Directory.Packages.props ├── Mellon.MultiTenant.sln ├── .gitignore ├── pubdev.ruleset ├── .editorconfig └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pub-Dev/Mellon.MultiTenant/HEAD/logo.png -------------------------------------------------------------------------------- /samples/Api/Migrations/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | generated_code = true 3 | dotnet_analyzer_diagnostic.severity = none -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Enums/TenantSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base.Enums; 2 | 3 | public enum TenantSource 4 | { 5 | EnvironmentVariables, 6 | Settings, 7 | Endpoint, 8 | } 9 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Exceptions/TenantNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base.Exceptions; 2 | 3 | internal class TenantNotFoundException(string message) : 4 | Exception($"Tenant {message} not found") 5 | { 6 | } -------------------------------------------------------------------------------- /samples/Api/appsettings.client-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client A", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-a;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Api/appsettings.client-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client B", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-b;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Api/appsettings.client-c.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client C", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-c;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Hangfire/appsettings.client-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client A", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-a;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Hangfire/appsettings.client-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client B", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-b;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Hangfire/appsettings.client-c.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "Message from Client C", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost,1433;Database=mellon-api-client-c;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/EndpointSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | public class EndpointSettings 4 | { 5 | public string Url { get; set; } 6 | 7 | public string Method { get; set; } 8 | 9 | public string Authorization { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Interfaces/IMultiTenantConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base.Interfaces; 2 | 3 | using Microsoft.Extensions.Configuration; 4 | 5 | public interface IMultiTenantConfiguration : IConfiguration 6 | { 7 | public string Tenant { get; } 8 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Exceptions/TenantSourceNotSetException.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base.Exceptions; 2 | 3 | using Mellon.MultiTenant.Base.Enums; 4 | 5 | internal class TenantSourceNotSetException(TenantSource source) : 6 | Exception($"{source} not set!") 7 | { 8 | } -------------------------------------------------------------------------------- /samples/Api/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Entities; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | public class Product 6 | { 7 | [Key] 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public int Quantity { get; set; } 13 | } -------------------------------------------------------------------------------- /samples/Hangfire/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Hangfire": { 4 | "commandName": "Project", 5 | "applicationUrl": "https://localhost:7299", 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Interfaces/IMultiTenantConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base.Interfaces; 2 | 3 | using Microsoft.Extensions.Configuration; 4 | 5 | public interface IMultiTenantConfigurationSource 6 | { 7 | IConfigurationBuilder AddSource(string tenant, IConfigurationBuilder builder); 8 | } -------------------------------------------------------------------------------- /samples/Hangfire/Jobs/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace WebApiHangfire.Jobs; 2 | 3 | using Hangfire; 4 | using Hangfire.Server; 5 | 6 | [Queue("tenant-name")] 7 | public interface IEmailSender 8 | { 9 | Task ExecuteAsync(PerformContext context); 10 | 11 | Task ExecuteLongJobAsync(int name, PerformContext context); 12 | } -------------------------------------------------------------------------------- /samples/Api/DataBaseContext.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using WebApi.Entities; 5 | 6 | public class DataBaseContext : DbContext 7 | { 8 | public DataBaseContext(DbContextOptions options) 9 | : base(options) 10 | { 11 | } 12 | 13 | public DbSet Products { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /samples/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WebAPI": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development", 8 | "MULTITENANT_TENANTS": "client-a,client-b,client-c" 9 | }, 10 | "applicationUrl": "https://localhost:63474" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Azure/AzureMultiTenantOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Azure; 2 | 3 | using Microsoft.Extensions.Configuration.AzureAppConfiguration; 4 | 5 | public class AzureMultiTenantOptions 6 | { 7 | public string AzureAppConfigurationConnectionString { get; set; } 8 | 9 | public Func> AzureAppConfigurationOptions { get; set; } 10 | } -------------------------------------------------------------------------------- /samples/Api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | sql-server: 4 | container_name: mellon-sql 5 | user: root 6 | image: mcr.microsoft.com/mssql/server 7 | volumes: 8 | - mellon-mssql-server-linux-data:/var/opt/mssql/data 9 | environment: 10 | MSSQL_SA_PASSWORD: "Pass@word" 11 | ACCEPT_EULA: "Y" 12 | ports: 13 | - "1433:1433" 14 | volumes: 15 | mellon-mssql-server-linux-data: 16 | driver: local 17 | -------------------------------------------------------------------------------- /samples/Hangfire/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | sql-server: 4 | container_name: mellon-hangfire-sql 5 | user: root 6 | image: mcr.microsoft.com/mssql/server 7 | volumes: 8 | - mellon-mssql-server-linux-data:/var/opt/mssql/data 9 | environment: 10 | MSSQL_SA_PASSWORD: "Pass@word" 11 | ACCEPT_EULA: "Y" 12 | ports: 13 | - "1433:1433" 14 | volumes: 15 | mellon-mssql-server-linux-data: 16 | driver: local 17 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(SolutionDir) 4 | pubdev.ruleset 5 | true 6 | true 7 | true 8 | MA0004;NU1507 9 | 10 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Interfaces/IMultiTenantRecurringJobManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.Interfaces; 2 | 3 | using global::Hangfire; 4 | using global::Hangfire.Common; 5 | 6 | public interface IMultiTenantRecurringJobManager : IRecurringJobManager 7 | { 8 | public void AddOrUpdateForAllTenants(string recurringJobId, Job job, string cronExpression, RecurringJobOptions options); 9 | 10 | public void RemoveIfExistsForAllTenants(string recurringJobId); 11 | 12 | public void TriggerForAllTenants(string recurringJobId); 13 | } -------------------------------------------------------------------------------- /samples/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Message": "hello", 3 | "MultiTenant": { 4 | "TenantSource": "Settings", 5 | "ApplicationName": "test2", 6 | "HttpHeaderKey": "x-tenant-name", 7 | "CookieKey": "tenant-name", 8 | "QueryStringKey": "tenant-name", 9 | "Tenants": [ 10 | "client-a", 11 | "client-b", 12 | "client-c" 13 | ] 14 | }, 15 | "Spring": { 16 | "Cloud": { 17 | "Config": { 18 | "Uri": "http://localhost:8888", 19 | "FailFast": false 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant/LocalMultiTenantSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant; 2 | 3 | using Mellon.MultiTenant.Base.Interfaces; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | public class LocalMultiTenantSource(IHostEnvironment hostEnvironment) : IMultiTenantConfigurationSource 8 | { 9 | public IConfigurationBuilder AddSource( 10 | string tenant, 11 | IConfigurationBuilder builder) 12 | { 13 | builder.AddJsonFile($"appsettings.{tenant}.json", optional: true); 14 | builder.AddJsonFile($"appsettings.{tenant}.{hostEnvironment.EnvironmentName}.json", optional: true); 15 | 16 | return builder; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/Hangfire/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "MultiTenant": { 8 | "TenantSource": "Settings", 9 | "ApplicationName": "test2", 10 | "HttpHeaderKey": "x-tenant-name", 11 | "CookieKey": "tenant-name", 12 | "QueryStringKey": "tenant-name", 13 | "SkipTenantCheckPaths": [ 14 | "^/hangfire.*" 15 | ], 16 | "Tenants": [ 17 | "client-a", 18 | "client-b", 19 | "client-c" 20 | ] 21 | }, 22 | "ConnectionStrings": { 23 | "HangFire": "Server=localhost,1433;Database=mellon-hangfire;User Id=sa;Password=Pass@word;TrustServerCertificate=True" 24 | } 25 | } -------------------------------------------------------------------------------- /samples/Api/WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/TenantSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | using Mellon.MultiTenant.Base.Exceptions; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | public class TenantSettings(MultiTenantSettings multiTenantSettings) 7 | { 8 | private readonly MultiTenantSettings _multiTenantSettings = multiTenantSettings; 9 | 10 | public string Tenant { get; private set; } 11 | 12 | public IConfiguration Configuration => Tenant is null ? null : _multiTenantSettings.GetConfigurations[Tenant]; 13 | 14 | public void SetCurrentTenant(string tenant) 15 | { 16 | if (_multiTenantSettings.GetConfigurations.ContainsKey(tenant)) 17 | { 18 | Tenant = tenant; 19 | } 20 | else 21 | { 22 | throw new TenantNotFoundException(tenant); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /samples/Hangfire/WebApiHangfire.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.ConfigServer/Extensions/MultiTenantExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Microsoft.Extensions.DependencyInjection; 3 | #pragma warning restore IDE0130 // Namespace does not match folder structure 4 | 5 | using Mellon.MultiTenant.Base.Interfaces; 6 | using Mellon.MultiTenant.ConfigServer; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | 9 | public static class MultiTenantExtensions 10 | { 11 | public static IServiceCollection AddMultiTenantSpringCloudConfig( 12 | this IServiceCollection services) 13 | { 14 | services.RemoveAll(); 15 | 16 | services.AddSingleton(); 17 | 18 | return services; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/TenantConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | using Mellon.MultiTenant.Base.Interfaces; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | public class TenantConfiguration( 8 | TenantSettings tenantSettings, 9 | IConfiguration configuration) : IMultiTenantConfiguration 10 | { 11 | public string Tenant { get; } = tenantSettings.Tenant; 12 | 13 | public IConfiguration Configuration { get; } = tenantSettings.Configuration ?? configuration; 14 | 15 | public string this[string key] { get => Configuration[key]; set => Configuration[key] = value; } 16 | 17 | public IEnumerable GetChildren() => Configuration.GetChildren(); 18 | 19 | public IChangeToken GetReloadToken() => Configuration.GetReloadToken(); 20 | 21 | public IConfigurationSection GetSection(string key) => Configuration.GetSection(key); 22 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.ConfigServer/ConfigServerMultiTenantSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.ConfigServer; 2 | 3 | using Mellon.MultiTenant.Base; 4 | using Mellon.MultiTenant.Base.Interfaces; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Hosting; 7 | using Steeltoe.Extensions.Configuration.ConfigServer; 8 | using Steeltoe.Extensions.Configuration.Placeholder; 9 | 10 | public class ConfigServerMultiTenantSource( 11 | MultiTenantOptions multiTenantOptions, 12 | IHostEnvironment hostEnvironment) : IMultiTenantConfigurationSource 13 | { 14 | public IConfigurationBuilder AddSource( 15 | string tenant, 16 | IConfigurationBuilder builder) 17 | { 18 | builder 19 | .AddConfigServer( 20 | hostEnvironment.EnvironmentName, 21 | $"{multiTenantOptions.ApplicationName ?? hostEnvironment.ApplicationName}-{tenant}") 22 | .AddPlaceholderResolver(); 23 | 24 | return builder; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/JobActivators/MultiTenantHangfireJobActivator.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.JobActivators; 2 | 3 | using global::Hangfire; 4 | using Mellon.MultiTenant.Base; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using HangfireAspNetCore = global::Hangfire.AspNetCore; 7 | 8 | public class MultiTenantHangfireJobActivator( 9 | IServiceScopeFactory serviceScopeFactory) 10 | : HangfireAspNetCore.AspNetCoreJobActivator(serviceScopeFactory) 11 | { 12 | public override JobActivatorScope BeginScope(JobActivatorContext context) 13 | { 14 | var scope = base.BeginScope(context); 15 | 16 | var tenantName = context.GetJobParameter("TenantName"); 17 | 18 | if (!string.IsNullOrEmpty(tenantName)) 19 | { 20 | var tenantSettings = (TenantSettings)scope.Resolve(typeof(TenantSettings)); 21 | 22 | tenantSettings.SetCurrentTenant(tenantName); 23 | } 24 | 25 | return scope; 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Humberto Rodrigues 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/Hangfire/Jobs/EmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace WebApiHangfire.Jobs; 2 | 3 | using Hangfire.Console; 4 | using Hangfire.Server; 5 | using Hangfire.Tags; 6 | using Mellon.MultiTenant.Base.Interfaces; 7 | 8 | public class EmailSender(ILogger logger, 9 | IMultiTenantConfiguration multiTenantConfiguration) : IEmailSender 10 | { 11 | public Task ExecuteAsync(PerformContext context) 12 | { 13 | context.AddTags(multiTenantConfiguration.Tenant); 14 | 15 | logger.LogInformation($"Processing e-mail sending for {multiTenantConfiguration.Tenant}"); 16 | 17 | return Task.FromResult(true); 18 | } 19 | 20 | public async Task ExecuteLongJobAsync(int name, PerformContext context) 21 | { 22 | context.AddTags(multiTenantConfiguration.Tenant); 23 | 24 | var items = new List() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; 25 | 26 | var bar = context.WriteProgressBar(); 27 | 28 | foreach (var item in items.WithProgress(bar)) 29 | { 30 | context.WriteLine(ConsoleTextColor.Gray, $"Starting the process for the object {item}"); 31 | 32 | await Task.Delay(2000, context.CancellationToken.ShutdownToken); 33 | 34 | context.WriteLine(ConsoleTextColor.Blue, $"process for the object {item} completed!"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /VersionConfig.yml: -------------------------------------------------------------------------------- 1 | next-version: 1.0.0 2 | branches: 3 | main: 4 | regex: ^master$|^main$ 5 | mode: ContinuousDelivery 6 | label: "" 7 | increment: Patch 8 | prevent-increment-of-merged-branch-version: true 9 | track-merge-target: false 10 | source-branches: ["develop", "release"] 11 | tracks-release-branches: false 12 | is-release-branch: false 13 | is-mainline: true 14 | pre-release-weight: 55000 15 | feature: 16 | regex: ^features?[/-] 17 | mode: ContinuousDelivery 18 | label: "{BranchName}" 19 | increment: Inherit 20 | source-branches: 21 | ["develop", "main", "release", "feature", "support", "hotfix"] 22 | pre-release-weight: 30000 23 | pull-request: 24 | regex: ^(pull|pull\-requests|pr)[/-] 25 | mode: ContinuousDelivery 26 | label: PullRequest 27 | increment: Inherit 28 | label-number-pattern: '[/-](?\d+)[-/]' 29 | source-branches: 30 | ["develop", "main", "release", "feature", "support", "hotfix"] 31 | pre-release-weight: 30000 32 | hotfix: 33 | regex: ^hotfix(es)?[/-] 34 | mode: ContinuousDelivery 35 | label: beta 36 | increment: Inherit 37 | source-branches: ["release", "main", "support", "hotfix"] 38 | pre-release-weight: 30000 39 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Attributes/PreventConcurrentExecutionJobFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.Attributes; 2 | 3 | using global::Hangfire; 4 | using global::Hangfire.Client; 5 | using global::Hangfire.Common; 6 | using global::Hangfire.Server; 7 | using Mellon.MultiTenant.Base; 8 | 9 | public class PreventConcurrentExecutionJobFilterAttribute : JobFilterAttribute, IClientFilter, IServerFilter 10 | { 11 | public void OnCreating(CreatingContext filterContext) 12 | { 13 | var processingJobs = JobStorage.Current.GetMonitoringApi().ProcessingJobs(0, 100); 14 | 15 | if (processingJobs.Any(x => string.Equals(GetResource(x.Value.Job), GetResource(filterContext.Job), StringComparison.InvariantCultureIgnoreCase))) 16 | { 17 | filterContext.SetJobParameter("Reason", "Job was already running"); 18 | 19 | filterContext.Canceled = true; 20 | } 21 | } 22 | 23 | public void OnPerforming(PerformingContext filterContext) 24 | { 25 | } 26 | 27 | public void OnPerformed(PerformedContext filterContext) 28 | { 29 | } 30 | 31 | public void OnCreated(CreatedContext filterContext) 32 | { 33 | } 34 | 35 | private static string GetResource(Job job) => $"{job.Queue}-{job.Type.ToGenericTypeString()}-{job.Method}-{string.Join('-', job.Args)}"; 36 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Azure/AzureTenantSource.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Azure; 2 | 3 | using Mellon.MultiTenant.Base.Interfaces; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | public class AzureTenantSource( 7 | IServiceProvider serviceProvider, 8 | AzureMultiTenantOptions azureMultiTenantOptions) : IMultiTenantConfigurationSource 9 | { 10 | public IConfigurationBuilder AddSource( 11 | string tenant, 12 | IConfigurationBuilder builder) 13 | { 14 | if (azureMultiTenantOptions.AzureAppConfigurationOptions is null) 15 | { 16 | if (azureMultiTenantOptions.AzureAppConfigurationConnectionString is null) 17 | { 18 | throw new Exception($"AzureAppConfigurationOptions is required when using Azure"); 19 | } 20 | 21 | builder.AddAzureAppConfiguration(options => 22 | options 23 | .Connect(azureMultiTenantOptions.AzureAppConfigurationConnectionString) 24 | .Select("*", tenant)); 25 | } 26 | else 27 | { 28 | builder.AddAzureAppConfiguration(azureMultiTenantOptions.AzureAppConfigurationOptions(serviceProvider, tenant)); 29 | } 30 | 31 | return builder; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/Api/Migrations/20230102162741_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace WebApi.Migrations 6 | { 7 | /// 8 | public partial class InitialMigration : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Products", 15 | columns: table => new 16 | { 17 | Id = table.Column(type: "int", nullable: false) 18 | .Annotation("SqlServer:Identity", "1, 1"), 19 | Name = table.Column(type: "nvarchar(max)", nullable: false), 20 | Quantity = table.Column(type: "int", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Products", x => x.Id); 25 | }); 26 | } 27 | 28 | /// 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropTable( 32 | name: "Products"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Azure/Extensions/MultiTenantExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Microsoft.Extensions.DependencyInjection; 3 | #pragma warning restore IDE0130 // Namespace does not match folder structure 4 | 5 | using Mellon.MultiTenant.Azure; 6 | using Mellon.MultiTenant.Base.Interfaces; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection.Extensions; 9 | 10 | public static class MultiTenantExtensions 11 | { 12 | public static IServiceCollection AddMultiTenantAzureAppConfiguration( 13 | this IServiceCollection services, 14 | Action action = null) 15 | { 16 | services.AddSingleton(serviceProvider => 17 | { 18 | var configuration = serviceProvider.GetRequiredService(); 19 | 20 | var azureMultiTenantOptions = new AzureMultiTenantOptions 21 | { 22 | AzureAppConfigurationConnectionString = configuration["AzureAppConfigurationConnectionString"] 23 | }; 24 | 25 | action?.Invoke(azureMultiTenantOptions); 26 | 27 | return azureMultiTenantOptions; 28 | }); 29 | 30 | services.RemoveAll(); 31 | 32 | services.AddSingleton(); 33 | 34 | return services; 35 | } 36 | } -------------------------------------------------------------------------------- /.github/workflows/jekyll-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v2 32 | - name: Build with Jekyll 33 | uses: actions/jekyll-build-pages@v1 34 | with: 35 | source: ./ 36 | destination: ./_site 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v1 39 | 40 | # Deployment job 41 | deploy: 42 | environment: 43 | name: github-pages 44 | url: ${{ steps.deployment.outputs.page_url }} 45 | runs-on: ubuntu-latest 46 | needs: build 47 | steps: 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v1 51 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Extensions/MultiTenantExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Microsoft.Extensions.DependencyInjection; 3 | #pragma warning restore IDE0130 // Namespace does not match folder structure 4 | 5 | using Hangfire; 6 | using Mellon.MultiTenant.Hangfire.Filters; 7 | using Mellon.MultiTenant.Hangfire.Interfaces; 8 | using Mellon.MultiTenant.Hangfire.JobActivators; 9 | using Mellon.MultiTenant.Hangfire.JobManagers; 10 | 11 | public static class MultiTenantExtensions 12 | { 13 | public static IServiceCollection AddMultiTenantHangfire( 14 | this IServiceCollection services) 15 | { 16 | services.AddScoped(); 17 | 18 | services.AddScoped(); 19 | 20 | services.AddSingleton(); 21 | 22 | return services; 23 | } 24 | 25 | public static IGlobalConfiguration UseMultiTenant( 26 | this IGlobalConfiguration globalConfiguration, 27 | IServiceProvider serviceProvider) 28 | { 29 | globalConfiguration.UseFilter(serviceProvider.GetRequiredService()); 30 | 31 | globalConfiguration.UseActivator( 32 | new MultiTenantHangfireJobActivator(serviceProvider.GetRequiredService())); 33 | 34 | return globalConfiguration; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Mellon.MultiTenant.Base; 2 | using Mellon.MultiTenant.Base.Interfaces; 3 | using Microsoft.EntityFrameworkCore; 4 | using WebApi; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | builder.Services.AddMultiTenant(); 9 | 10 | builder.Services.AddDbContext( 11 | (IServiceProvider serviceProvider, DbContextOptionsBuilder options) => 12 | { 13 | var configuration = serviceProvider.GetRequiredService(); 14 | 15 | options.UseSqlServer(configuration?["ConnectionStrings:DefaultConnection"]); 16 | }); 17 | 18 | var app = builder.Build(); 19 | 20 | app.UseMultiTenant(); 21 | 22 | app.MapGet("/", (IMultiTenantConfiguration configuration) => new 23 | { 24 | configuration.Tenant, 25 | Message = configuration["Message"], 26 | }); 27 | 28 | app.MapGet("/products", async (DataBaseContext dataBaseContext) => await dataBaseContext.Products.ToListAsync(cancellationToken: app.Lifetime.ApplicationStopped)); 29 | 30 | var tenants = app.Services.GetRequiredService(); 31 | 32 | foreach (var tenant in tenants.Tenants) 33 | { 34 | using var scope = app.Services.CreateScope(); 35 | 36 | var tenantSettings = scope.ServiceProvider.GetRequiredService(); 37 | 38 | tenantSettings.SetCurrentTenant(tenant); 39 | 40 | var db = scope.ServiceProvider.GetRequiredService(); 41 | 42 | await db.Database.MigrateAsync(cancellationToken: app.Lifetime.ApplicationStopped); 43 | } 44 | 45 | await app.RunAsync(); -------------------------------------------------------------------------------- /src/Mellon.MultiTenant/Extensions/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Extensions; 2 | 3 | using Mellon.MultiTenant.Base; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | internal static class HttpContextExtensions 7 | { 8 | internal static bool TryExtractTenantFromHttpContext( 9 | this HttpContext context, 10 | MultiTenantOptions multiTenantOptions, 11 | out string tenant) 12 | { 13 | if (multiTenantOptions.GetTenantFromHttClientFunc is not null) 14 | { 15 | tenant = multiTenantOptions.GetTenantFromHttClientFunc(context); 16 | 17 | return true; 18 | } 19 | 20 | if (multiTenantOptions.HttpHeaderKey is not null && 21 | context.Request.Headers.TryGetValue(multiTenantOptions.HttpHeaderKey, out var header)) 22 | { 23 | tenant = header; 24 | 25 | return true; 26 | } 27 | 28 | if (multiTenantOptions.QueryStringKey is not null && 29 | context.Request.Query.TryGetValue(multiTenantOptions.QueryStringKey, out var queryString)) 30 | { 31 | tenant = queryString; 32 | 33 | return true; 34 | } 35 | 36 | if (multiTenantOptions.CookieKey is not null && 37 | context.Request.Cookies.TryGetValue(multiTenantOptions.CookieKey, out var cookie)) 38 | { 39 | tenant = cookie; 40 | 41 | return true; 42 | } 43 | 44 | tenant = null; 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/Api/Migrations/DataBaseContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using WebApi; 7 | 8 | #nullable disable 9 | 10 | namespace WebApi.Migrations 11 | { 12 | [DbContext(typeof(DataBaseContext))] 13 | partial class DataBaseContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "7.0.1") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 21 | 22 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 23 | 24 | modelBuilder.Entity("WebApi.Product", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("int"); 29 | 30 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 31 | 32 | b.Property("Name") 33 | .IsRequired() 34 | .HasColumnType("nvarchar(max)"); 35 | 36 | b.Property("Quantity") 37 | .HasColumnType("int"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.ToTable("MyModels"); 42 | }); 43 | #pragma warning restore 612, 618 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Interfaces/IMultiTenantBackgroundJobManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.Interfaces; 2 | 3 | using System.Linq.Expressions; 4 | 5 | public interface IMultiTenantBackgroundJobManager 6 | { 7 | IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression methodCall); 8 | 9 | IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall); 10 | 11 | IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall); 12 | 13 | IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall); 14 | 15 | string Enqueue(Expression methodCall); 16 | 17 | string Enqueue(Expression> methodCall); 18 | 19 | string Enqueue(Expression> methodCall); 20 | 21 | string Enqueue(Expression> methodCall); 22 | 23 | IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression methodCall, TimeSpan delay); 24 | 25 | IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay); 26 | 27 | IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay); 28 | 29 | IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay); 30 | 31 | string Schedule(Expression methodCall, TimeSpan delay); 32 | 33 | string Schedule(Expression> methodCall, TimeSpan delay); 34 | 35 | string Schedule(Expression> methodCall, TimeSpan delay); 36 | 37 | string Schedule(Expression> methodCall, TimeSpan delay); 38 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant/Mellon.MultiTenant.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | Mellon.MultiTenant 7 | Mellon-MultiTenant 8 | 1berto and Naga 9 | PubDev 10 | A Package to create Multi-Tenant Applications using NET 11 | MIT 12 | https://1bberto.github.io/Mellon.MultiTenant/ 13 | https://github.com/1bberto/Mellon.MultiTenant 14 | git 15 | false 16 | logo.png 17 | readme.md 18 | true 19 | false 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | Mellon.MultiTenant.Azure 7 | Mellon-MultiTenant-Azure 8 | 1berto and Naga 9 | PubDev 10 | A Package to create Multi-Tenant Applications using NET 11 | MIT 12 | https://pub-dev.github.io/Mellon.MultiTenant/ 13 | https://github.com/1bberto/Mellon.MultiTenant 14 | git 15 | false 16 | logo.png 17 | readme.md 18 | true 19 | false 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | Mellon.MultiTenant.Hangfire 7 | Mellon-MultiTenant-Hangfire 8 | 1berto and Naga 9 | PubDev 10 | A Package to create Multi-Tenant Applications using NET 11 | MIT 12 | https://pub-dev.github.io/Mellon.MultiTenant/ 13 | https://github.com/1bberto/Mellon.MultiTenant 14 | git 15 | false 16 | logo.png 17 | readme.md 18 | true 19 | false 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /samples/Api/Migrations/20230102162741_InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using WebApi; 8 | 9 | #nullable disable 10 | 11 | namespace WebApi.Migrations 12 | { 13 | [DbContext(typeof(DataBaseContext))] 14 | [Migration("20230102162741_InitialMigration")] 15 | partial class InitialMigration 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "7.0.1") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 24 | 25 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("WebApi.Product", b => 28 | { 29 | b.Property("Id") 30 | .ValueGeneratedOnAdd() 31 | .HasColumnType("int"); 32 | 33 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 34 | 35 | b.Property("Name") 36 | .IsRequired() 37 | .HasColumnType("nvarchar(max)"); 38 | 39 | b.Property("Quantity") 40 | .HasColumnType("int"); 41 | 42 | b.HasKey("Id"); 43 | 44 | b.ToTable("MyModels"); 45 | }); 46 | #pragma warning restore 612, 618 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | Mellon.MultiTenant.ConfigServer 7 | Mellon-MultiTenant-ConfigServer 8 | 1berto and Naga 9 | PubDev 10 | A Package to create Multi-Tenant Applications using NET 11 | MIT 12 | https://pub-dev.github.io/Mellon.MultiTenant/ 13 | https://github.com/1bberto/Mellon.MultiTenant 14 | git 15 | false 16 | logo.png 17 | readme.md 18 | true 19 | false 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | Mellon.MultiTenant.Base 7 | Mellon-MultiTenant-Base 8 | 1berto and Naga 9 | PubDev 10 | A Package to create Multi-Tenant Applications using NET 11 | MIT 12 | https://pub-dev.github.io/Mellon.MultiTenant/ 13 | https://github.com/1bberto/Mellon.MultiTenant 14 | git 15 | false 16 | logo.png 17 | readme.md 18 | true 19 | false 20 | true 21 | snupkg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Extensions/MultiTenantRecurringJobManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Mellon.MultiTenant.Extensions; 3 | #pragma warning restore IDE0130 // Namespace does not match folder structure 4 | 5 | using System.Linq.Expressions; 6 | using global::Hangfire; 7 | using global::Hangfire.Common; 8 | using Mellon.MultiTenant.Hangfire.Interfaces; 9 | 10 | public static class MultiTenantRecurringJobManagerExtensions 11 | { 12 | public static void AddOrUpdate( 13 | this IMultiTenantRecurringJobManager manager, 14 | string recurringJobId, 15 | Expression> methodCall, 16 | string cronExpression, 17 | TimeZoneInfo timeZone = null, 18 | string queue = "default") 19 | { 20 | ArgumentNullException.ThrowIfNull(manager); 21 | 22 | ArgumentNullException.ThrowIfNull(recurringJobId); 23 | 24 | ArgumentNullException.ThrowIfNull(methodCall); 25 | 26 | ArgumentNullException.ThrowIfNull(cronExpression); 27 | 28 | timeZone ??= TimeZoneInfo.Utc; 29 | 30 | Job job = Job.FromExpression(methodCall, queue); 31 | 32 | manager.AddOrUpdate(recurringJobId, job, cronExpression, new RecurringJobOptions 33 | { 34 | TimeZone = timeZone 35 | }); 36 | } 37 | 38 | public static void AddOrUpdateForAllTenants( 39 | this IMultiTenantRecurringJobManager manager, 40 | string recurringJobId, 41 | Expression> methodCall, 42 | string cronExpression, 43 | TimeZoneInfo timeZone = null, 44 | string queue = "default") 45 | { 46 | ArgumentNullException.ThrowIfNull(manager); 47 | 48 | ArgumentNullException.ThrowIfNull(recurringJobId); 49 | 50 | ArgumentNullException.ThrowIfNull(methodCall); 51 | 52 | ArgumentNullException.ThrowIfNull(cronExpression); 53 | 54 | var job = Job.FromExpression(methodCall, queue); 55 | 56 | manager.AddOrUpdateForAllTenants(recurringJobId, job, cronExpression, timeZone ?? TimeZoneInfo.Utc); 57 | } 58 | 59 | public static void AddOrUpdateForAllTenants( 60 | this IMultiTenantRecurringJobManager manager, 61 | string recurringJobId, 62 | Job job, 63 | string cronExpression, 64 | TimeZoneInfo timeZone) 65 | { 66 | ArgumentNullException.ThrowIfNull(manager); 67 | 68 | ArgumentNullException.ThrowIfNull(timeZone); 69 | 70 | manager.AddOrUpdateForAllTenants(recurringJobId, job, cronExpression, new RecurringJobOptions 71 | { 72 | TimeZone = timeZone, 73 | }); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant/Middlewares/HttpTenantIdentifierMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Middlewares; 2 | 3 | using System.Text.RegularExpressions; 4 | using Mellon.MultiTenant.Base; 5 | using Mellon.MultiTenant.Extensions; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | public partial class HttpTenantIdentifierMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | 13 | private readonly List _regexes = []; 14 | 15 | public HttpTenantIdentifierMiddleware( 16 | RequestDelegate next, 17 | MultiTenantOptions multiTenantOptions) 18 | { 19 | _next = next; 20 | 21 | if (multiTenantOptions.SkipTenantCheckPaths != null && multiTenantOptions.SkipTenantCheckPaths.Count > 0) 22 | { 23 | LoadRegexes(multiTenantOptions.SkipTenantCheckPaths); 24 | } 25 | 26 | _regexes.Add(RefreshSettingsRegex()); 27 | } 28 | 29 | public Task InvokeAsync(HttpContext context) 30 | { 31 | var tenantSettings = context.RequestServices.GetRequiredService(); 32 | 33 | var multiTenantOptions = context.RequestServices.GetRequiredService(); 34 | 35 | if (context.TryExtractTenantFromHttpContext(multiTenantOptions, out var tenant)) 36 | { 37 | tenantSettings.SetCurrentTenant(tenant); 38 | } 39 | 40 | if (tenantSettings.Tenant is null && multiTenantOptions.DefaultTenant is not null) 41 | { 42 | tenantSettings.SetCurrentTenant(multiTenantOptions.DefaultTenant); 43 | } 44 | 45 | // only need to throw an exception if the path is not the refresh endpoint or path is whitelisted when the tenant is not defined 46 | if (!IsWhiteListedPath(context.Request.Path)) 47 | { 48 | if (tenantSettings.Tenant is null) 49 | { 50 | throw new Exception("Tenant not identified!"); 51 | } 52 | } 53 | 54 | return _next(context); 55 | } 56 | 57 | [GeneratedRegex("^/refresh-settings.*", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 1000)] 58 | private static partial Regex RefreshSettingsRegex(); 59 | 60 | private void LoadRegexes(List paths) 61 | { 62 | foreach (var item in paths) 63 | { 64 | var regex = new Regex(item, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); 65 | 66 | _regexes.Add(regex); 67 | } 68 | } 69 | 70 | private bool IsWhiteListedPath(string path) 71 | { 72 | if (_regexes.Count == 0) 73 | { 74 | return true; 75 | } 76 | 77 | foreach (var regex in _regexes) 78 | { 79 | if (regex.IsMatch(path)) 80 | { 81 | return true; 82 | } 83 | } 84 | 85 | return false; 86 | } 87 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/MultiTenantSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | using Mellon.MultiTenant.Base.Enums; 4 | using Mellon.MultiTenant.Base.Exceptions; 5 | using Mellon.MultiTenant.Base.Interfaces; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | public class MultiTenantSettings 10 | { 11 | private readonly Dictionary _configurations = []; 12 | 13 | public static async Task LoadTenantsAsync( 14 | MultiTenantOptions multiTenantOptions, 15 | IConfiguration configuration) 16 | { 17 | switch (multiTenantOptions.TenantSource) 18 | { 19 | case TenantSource.EnvironmentVariables: 20 | if (configuration["MULTITENANT_TENANTS"] is null) 21 | { 22 | throw new TenantSourceNotSetException(TenantSource.EnvironmentVariables); 23 | } 24 | 25 | return configuration["MULTITENANT_TENANTS"].Split(',', StringSplitOptions.RemoveEmptyEntries); 26 | 27 | case TenantSource.Settings: 28 | if (configuration.GetSection("MultiTenant:Tenants") is null) 29 | { 30 | throw new TenantSourceNotSetException(TenantSource.Settings); 31 | } 32 | 33 | return configuration.GetSection("MultiTenant:Tenants").Get(); 34 | 35 | case TenantSource.Endpoint: 36 | var tenants = await multiTenantOptions.GetTenantSourceHttpEndpointFunc(multiTenantOptions.Endpoint, configuration); 37 | 38 | if (tenants.Length == 0) 39 | { 40 | throw new TenantSourceNotSetException(TenantSource.Endpoint); 41 | } 42 | 43 | return tenants; 44 | 45 | default: 46 | throw new TenantSourceNotSetException(multiTenantOptions.TenantSource); 47 | } 48 | } 49 | 50 | public static IConfigurationRoot BuildTenantConfiguration( 51 | IHostEnvironment hostEnvironment, 52 | IMultiTenantConfigurationSource tenantConfigurationSource, 53 | string tenant) 54 | { 55 | var builder = new ConfigurationBuilder() 56 | .AddJsonFile("appsettings.json", true) 57 | .AddJsonFile($"appsettings.{hostEnvironment.EnvironmentName}.json", true) 58 | .AddEnvironmentVariables(); 59 | 60 | builder = tenantConfigurationSource.AddSource(tenant, builder); 61 | 62 | return builder.Build(); 63 | } 64 | 65 | public IReadOnlyList Tenants => _configurations?.Keys.ToList(); 66 | 67 | public IReadOnlyDictionary GetConfigurations => _configurations; 68 | 69 | public void LoadConfiguration(string tenant, IConfigurationRoot configuration) 70 | { 71 | if (!_configurations.ContainsKey(tenant)) 72 | { 73 | _configurations[tenant] = configuration; 74 | } 75 | else 76 | { 77 | throw new Exception($"Tenant {tenant} already configured"); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/JobManagers/MultiTenantRecurringJobManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.JobManagers; 2 | 3 | using global::Hangfire; 4 | using global::Hangfire.Common; 5 | using Mellon.MultiTenant.Base; 6 | using Mellon.MultiTenant.Base.Interfaces; 7 | using Mellon.MultiTenant.Hangfire.Interfaces; 8 | 9 | internal class MultiTenantRecurringJobManager( 10 | IRecurringJobManager recurringJobManager, 11 | IMultiTenantConfiguration multiTenantConfiguration, 12 | MultiTenantSettings multiTenantSettings) : IMultiTenantRecurringJobManager 13 | { 14 | public void AddOrUpdate(string recurringJobId, Job job, string cronExpression, RecurringJobOptions options) 15 | { 16 | var jobName = GetTenantJobName(multiTenantConfiguration.Tenant, recurringJobId); 17 | 18 | recurringJobManager.AddOrUpdate(jobName, job, cronExpression, options); 19 | } 20 | 21 | public void AddOrUpdateForAllTenants(string recurringJobId, Job job, string cronExpression, RecurringJobOptions options) 22 | { 23 | foreach (var tenant in multiTenantSettings.Tenants) 24 | { 25 | var jobName = GetTenantJobName(tenant, recurringJobId); 26 | 27 | recurringJobManager.AddOrUpdate(jobName, job, cronExpression, options); 28 | } 29 | } 30 | 31 | public void RemoveIfExists(string recurringJobId) 32 | { 33 | var jobName = GetTenantJobName(multiTenantConfiguration.Tenant, recurringJobId); 34 | 35 | recurringJobManager.RemoveIfExists(jobName); 36 | } 37 | 38 | public void RemoveIfExistsForAllTenants(string recurringJobId) 39 | { 40 | foreach (var tenant in multiTenantSettings.Tenants) 41 | { 42 | var jobName = GetTenantJobName(tenant, recurringJobId); 43 | 44 | recurringJobManager.RemoveIfExists(jobName); 45 | } 46 | } 47 | 48 | public void Trigger(string recurringJobId) 49 | { 50 | var jobName = GetTenantJobName(multiTenantConfiguration.Tenant, recurringJobId); 51 | 52 | recurringJobManager.Trigger(jobName); 53 | } 54 | 55 | public void TriggerForAllTenants(string recurringJobId) 56 | { 57 | foreach (var tenant in multiTenantSettings.Tenants) 58 | { 59 | var jobName = GetTenantJobName(tenant, recurringJobId); 60 | 61 | recurringJobManager.Trigger(jobName); 62 | } 63 | } 64 | 65 | private static string GetTenantJobName(string tenant, string recurringJobId) 66 | { 67 | if (recurringJobId.Contains($"{tenant}@", StringComparison.InvariantCultureIgnoreCase)) 68 | { 69 | return recurringJobId; 70 | } 71 | 72 | return $"{tenant}@{recurringJobId}"; 73 | } 74 | } -------------------------------------------------------------------------------- /samples/Hangfire/Program.cs: -------------------------------------------------------------------------------- 1 | using Hangfire; 2 | using Hangfire.Console; 3 | using Hangfire.Tags; 4 | using Hangfire.Tags.SqlServer; 5 | using Mellon.MultiTenant.Base; 6 | using Mellon.MultiTenant.Base.Interfaces; 7 | using Mellon.MultiTenant.Extensions; 8 | using Mellon.MultiTenant.Hangfire.Interfaces; 9 | using WebApiHangfire.Jobs; 10 | 11 | var builder = WebApplication.CreateBuilder(args); 12 | 13 | builder.Services 14 | .AddMultiTenant() 15 | .AddMultiTenantHangfire(); 16 | 17 | builder.Services.AddHangfireServer((serviceProvider, config) => 18 | { 19 | var multiTenantSettings = serviceProvider.GetRequiredService(); 20 | var tenants = new List(multiTenantSettings.Tenants) 21 | { 22 | "cron", 23 | "default" 24 | }; 25 | 26 | config.ServerName = $"Worker-{Guid.NewGuid()}"; 27 | config.Queues = [.. tenants]; 28 | config.WorkerCount = 10; 29 | }); 30 | 31 | builder.Services.AddHangfire((serviceProvider, config) => 32 | { 33 | config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangFire")); 34 | config.UseColouredConsoleLogProvider(); 35 | config.UseSimpleAssemblyNameTypeSerializer(); 36 | config.UseRecommendedSerializerSettings(); 37 | config.UseConsole(); 38 | config.UseMultiTenant(serviceProvider); 39 | 40 | var tagsOptions = new TagsOptions() { TagsListStyle = TagsListStyle.Dropdown }; 41 | 42 | config.UseTagsWithSql(tagsOptions); 43 | }); 44 | 45 | builder.Services.AddScoped(); 46 | 47 | var app = builder.Build(); 48 | 49 | app.UseMultiTenant(); 50 | 51 | app.MapGet("add-email-sender", ( 52 | IMultiTenantRecurringJobManager recurringJobManager, 53 | IMultiTenantBackgroundJobManager multiTenantBackgroundJobManager, 54 | IMultiTenantConfiguration multiTenantConfiguration) => 55 | { 56 | // recurringJobManager.AddOrUpdateForAllTenants("email-sender", job => job.ExecuteAsync(), Cron.Minutely()); 57 | // recurringJobManager.AddOrUpdateForAllTenants("long-email-sender", job => job.ExecuteLongJobAsync(1, null), "*/2 * * * *"); 58 | multiTenantBackgroundJobManager.Enqueue(() => Console.WriteLine($"HELLO WORLD! for {multiTenantConfiguration.Tenant}")); 59 | 60 | recurringJobManager 61 | .AddOrUpdate("email-sender", job => job.ExecuteLongJobAsync(1, null), Cron.Minutely()); 62 | 63 | // multiTenantBackgroundJobManager.EnqueueForAllTenants(() => Console.WriteLine("HELLO WORLD! FOR ALL TENANTS")); 64 | 65 | // multiTenantBackgroundJobManager.Schedule(() => Console.WriteLine("HELLO WORLD! SINGLE 3 MINUTES"), TimeSpan.FromMinutes(3)); 66 | 67 | // multiTenantBackgroundJobManager.ScheduleForAllTenants(() => Console.WriteLine("HELLO WORLD! SCHEDULERD 3 MINUTES"), TimeSpan.FromMinutes(3)); 68 | return Results.Accepted(); 69 | }); 70 | 71 | app.UseHangfireDashboard(); 72 | 73 | await app.RunAsync(); -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | all 5 | runtime; build; native; contentfiles; analyzers; buildtransitive 6 | 7 | 8 | all 9 | runtime; build; native; contentfiles; analyzers; buildtransitive 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/Filters/MultiTenantClientFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.Filters; 2 | 3 | using global::Hangfire.Client; 4 | using global::Hangfire.Console; 5 | using global::Hangfire.Server; 6 | using global::Hangfire.States; 7 | using global::Hangfire.Storage; 8 | using Microsoft.Extensions.Logging; 9 | 10 | internal class MultiTenantClientFilter(ILogger logger) : IClientFilter, IServerFilter, IApplyStateFilter 11 | { 12 | public void OnCreating(CreatingContext filterContext) 13 | { 14 | var tenantName = ExtractTenantFromContext(filterContext); 15 | 16 | filterContext.SetJobParameter("TenantName", tenantName); 17 | } 18 | 19 | public void OnCreated(CreatedContext filterContext) 20 | { 21 | filterContext.Parameters.TryGetValue("RecurringJobId", out var jobId); 22 | 23 | filterContext.Parameters.TryGetValue("TenantName", out var tenantName); 24 | 25 | logger.LogInformation( 26 | "Job `{recurringJobId}` that is based on method `{Name}` has been created with id `{Id}` for Tenant `{tenant}`", 27 | jobId, 28 | filterContext.Job.Method.Name, 29 | filterContext.BackgroundJob?.Id, 30 | tenantName); 31 | } 32 | 33 | public void OnPerforming(PerformingContext filterContext) 34 | { 35 | var tenantName = filterContext.GetJobParameter("TenantName"); 36 | 37 | filterContext.WriteLine(ConsoleTextColor.Yellow, $"Starting job for the tenant {tenantName} 🚀"); 38 | } 39 | 40 | public void OnPerformed(PerformedContext filterContext) 41 | { 42 | var tenantName = filterContext.GetJobParameter("TenantName"); 43 | 44 | filterContext.WriteLine(ConsoleTextColor.Yellow, $"Process completed for the {tenantName} 🚀"); 45 | } 46 | 47 | public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 48 | { 49 | var queue = context.GetJobParameter("TenantName"); 50 | 51 | if (!string.IsNullOrWhiteSpace(queue)) 52 | { 53 | if (context.NewState is EnqueuedState newState) 54 | { 55 | if (string.Equals(newState.Queue, "tenant-name", StringComparison.InvariantCultureIgnoreCase)) 56 | { 57 | newState.Queue = queue; 58 | } 59 | } 60 | } 61 | } 62 | 63 | public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) 64 | { 65 | } 66 | 67 | private static string ExtractTenantFromContext(CreatingContext filterContext) 68 | { 69 | var tenantName = default(string); 70 | 71 | if (filterContext.Parameters.TryGetValue("RecurringJobId", out var jobId) && jobId.ToString().Contains('@', StringComparison.InvariantCultureIgnoreCase)) 72 | { 73 | tenantName = jobId.ToString().Split("@")[0]; 74 | } 75 | 76 | if (string.IsNullOrEmpty(tenantName) && filterContext.InitialState is EnqueuedState enqueuedState) 77 | { 78 | if (string.Equals(enqueuedState.Queue, EnqueuedState.DefaultQueue, StringComparison.InvariantCultureIgnoreCase)) 79 | { 80 | tenantName = filterContext.Job.Queue; 81 | } 82 | else 83 | { 84 | tenantName = enqueuedState.Queue; 85 | } 86 | } 87 | 88 | if (string.IsNullOrEmpty(tenantName) && filterContext.InitialState is ScheduledState) 89 | { 90 | tenantName = filterContext.Job.Queue; 91 | } 92 | 93 | return tenantName; 94 | } 95 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | using System.Reflection; 4 | using System.Text.RegularExpressions; 5 | 6 | public static partial class TypeExtensions 7 | { 8 | public static string ToGenericTypeString(this Type type) 9 | { 10 | if (!type.GetTypeInfo().IsGenericType) 11 | { 12 | return type.GetFullNameWithoutNamespace().ReplacePlusWithDotInNestedTypeName(); 13 | } 14 | 15 | return type 16 | .GetGenericTypeDefinition() 17 | .GetFullNameWithoutNamespace() 18 | .ReplacePlusWithDotInNestedTypeName() 19 | .ReplaceGenericParametersInGenericTypeName(type); 20 | } 21 | 22 | public static Type[] GetAllGenericArguments(this TypeInfo type) 23 | { 24 | if (type.GenericTypeArguments.Length == 0) 25 | { 26 | return type.GenericTypeParameters; 27 | } 28 | 29 | return type.GenericTypeArguments; 30 | } 31 | 32 | private static bool TypesMatchRecursive(TypeInfo parameterType, TypeInfo actualType, IList genericArguments) 33 | { 34 | if (parameterType.IsGenericParameter) 35 | { 36 | var genericParameterPosition = parameterType.GenericParameterPosition; 37 | 38 | if (genericArguments[genericParameterPosition] != null && genericArguments[genericParameterPosition].GetTypeInfo() != actualType) 39 | { 40 | return false; 41 | } 42 | 43 | genericArguments[genericParameterPosition] = actualType.AsType(); 44 | return true; 45 | } 46 | 47 | if (parameterType.ContainsGenericParameters) 48 | { 49 | if (parameterType.IsArray) 50 | { 51 | if (!actualType.IsArray) 52 | { 53 | return false; 54 | } 55 | 56 | var elementType = parameterType.GetElementType(); 57 | return TypesMatchRecursive(actualType: actualType.GetElementType().GetTypeInfo(), parameterType: elementType.GetTypeInfo(), genericArguments: genericArguments); 58 | } 59 | 60 | if (!actualType.IsGenericType || parameterType.GetGenericTypeDefinition() != actualType.GetGenericTypeDefinition()) 61 | { 62 | return false; 63 | } 64 | 65 | for (var i = 0; i < parameterType.GenericTypeArguments.Length; i++) 66 | { 67 | Type type = parameterType.GenericTypeArguments[i]; 68 | if (!TypesMatchRecursive(actualType: actualType.GenericTypeArguments[i].GetTypeInfo(), parameterType: type.GetTypeInfo(), genericArguments: genericArguments)) 69 | { 70 | return false; 71 | } 72 | } 73 | 74 | return true; 75 | } 76 | 77 | return parameterType == actualType; 78 | } 79 | 80 | private static string GetFullNameWithoutNamespace(this Type type) 81 | { 82 | if (type.IsGenericParameter) 83 | { 84 | return type.Name; 85 | } 86 | 87 | if (string.IsNullOrEmpty(type.Namespace)) 88 | { 89 | return type.FullName; 90 | } 91 | 92 | return type.FullName![(type.Namespace!.Length + 1)..]; 93 | } 94 | 95 | private static string ReplacePlusWithDotInNestedTypeName(this string typeName) => typeName.Replace('+', '.'); 96 | 97 | private static string ReplaceGenericParametersInGenericTypeName( 98 | this string typeName, Type type) 99 | { 100 | var genericArguments = type.GetTypeInfo().GetAllGenericArguments(); 101 | 102 | typeName = TypeNameRegex().Replace(typeName, match => 103 | { 104 | var count = int.Parse(match.Value[1..]); 105 | var text = string.Join(",", genericArguments.Take(count).Select(new Func(ToGenericTypeString))); 106 | genericArguments = genericArguments.Skip(count).ToArray(); 107 | return $"<{text}>"; 108 | }); 109 | 110 | return typeName; 111 | } 112 | 113 | [GeneratedRegex("`[1-9]\\d*", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 1000)] 114 | private static partial Regex TypeNameRegex(); 115 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Base/MultiTenantOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Base; 2 | 3 | using System.Net.Http.Json; 4 | using Mellon.MultiTenant.Base.Enums; 5 | using Mellon.MultiTenant.Base.Interfaces; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Configuration; 8 | 9 | public class MultiTenantOptions 10 | { 11 | private static readonly Lazy _httpClientLazy = new(() => new HttpClient(), LazyThreadSafetyMode.ExecutionAndPublication); 12 | 13 | public TenantSource TenantSource { get; set; } = TenantSource.Settings; 14 | 15 | public string ApplicationName { get; set; } 16 | 17 | public string HttpHeaderKey { get; set; } 18 | 19 | public string QueryStringKey { get; set; } 20 | 21 | public string CookieKey { get; set; } 22 | 23 | public string DefaultTenant { get; set; } 24 | 25 | public List SkipTenantCheckPaths { get; set; } 26 | 27 | public Func GetTenantFromHttClientFunc { get; private set; } 28 | 29 | public Type CustomMultiTenantConfigurationSource { get; private set; } 30 | 31 | public Func> GetTenantSourceHttpEndpointFunc { get; private set; } 32 | 33 | public EndpointSettings Endpoint { get; set; } 34 | 35 | public MultiTenantOptions LoadFromSettings() 36 | { 37 | TenantSource = TenantSource.Settings; 38 | 39 | return this; 40 | } 41 | 42 | public MultiTenantOptions LoadFromEnvironmentVariable() 43 | { 44 | TenantSource = TenantSource.EnvironmentVariables; 45 | 46 | return this; 47 | } 48 | 49 | public MultiTenantOptions LoadFromEndpoint(Func> func) 50 | { 51 | TenantSource = TenantSource.Endpoint; 52 | 53 | GetTenantSourceHttpEndpointFunc = func; 54 | 55 | return this; 56 | } 57 | 58 | public MultiTenantOptions LoadFromEndpoint(Func func) => 59 | LoadFromEndpoint(async (endpointOptions, configuration) => 60 | { 61 | var request = new HttpRequestMessage() 62 | { 63 | RequestUri = new Uri(endpointOptions.Url), 64 | Method = new HttpMethod(endpointOptions.Method ?? "GET"), 65 | }; 66 | 67 | if (!string.IsNullOrEmpty(endpointOptions.Authorization)) 68 | { 69 | request.Headers.Add("Authorization", endpointOptions.Authorization); 70 | } 71 | 72 | var result = await _httpClientLazy.Value.SendAsync(request); 73 | 74 | if (result.IsSuccessStatusCode) 75 | { 76 | var data = await result.Content.ReadFromJsonAsync>(); 77 | 78 | var tenants = data!.Select(func).ToArray(); 79 | 80 | if (tenants.Length == 0) 81 | { 82 | throw new Exception($"No tenant found on the endpoint {endpointOptions.Url}"); 83 | } 84 | 85 | return tenants; 86 | } 87 | else 88 | { 89 | var statusCode = result.StatusCode; 90 | 91 | var reason = result.ReasonPhrase; 92 | 93 | var content = await result.Content.ReadAsStringAsync(); 94 | 95 | throw new Exception($@"Error to load tenants from the url {endpointOptions.Url} StatusCode: {statusCode} Reason: {reason} Content: {content}"); 96 | } 97 | }); 98 | 99 | public MultiTenantOptions WithApplicationName(string applicationName) 100 | { 101 | ApplicationName = applicationName; 102 | 103 | return this; 104 | } 105 | 106 | public MultiTenantOptions WithHttpHeader(string httpHeader) 107 | { 108 | HttpHeaderKey = httpHeader; 109 | 110 | return this; 111 | } 112 | 113 | public MultiTenantOptions WithQueryString(string queryString) 114 | { 115 | QueryStringKey = queryString; 116 | 117 | return this; 118 | } 119 | 120 | public MultiTenantOptions WithCookie(string cookieName) 121 | { 122 | CookieKey = cookieName; 123 | 124 | return this; 125 | } 126 | 127 | public MultiTenantOptions WithHttpContextLoad(Func func) 128 | { 129 | GetTenantFromHttClientFunc = func; 130 | 131 | return this; 132 | } 133 | 134 | public MultiTenantOptions WithDefaultTenant(string defaultTenant) 135 | { 136 | DefaultTenant = defaultTenant; 137 | 138 | return this; 139 | } 140 | 141 | public MultiTenantOptions WithSkipTenantCheckPaths(string path) 142 | { 143 | SkipTenantCheckPaths.Add(path); 144 | 145 | return this; 146 | } 147 | 148 | public MultiTenantOptions WithSkipTenantCheckPaths(params string[] path) 149 | { 150 | SkipTenantCheckPaths.AddRange(path); 151 | 152 | return this; 153 | } 154 | 155 | public MultiTenantOptions WithCustomTenantConfigurationSource() where T : IMultiTenantConfigurationSource 156 | { 157 | CustomMultiTenantConfigurationSource = typeof(T); 158 | 159 | return this; 160 | } 161 | } -------------------------------------------------------------------------------- /src/Mellon.MultiTenant.Hangfire/JobManagers/MultiTenantBackgroundJobManager.cs: -------------------------------------------------------------------------------- 1 | namespace Mellon.MultiTenant.Hangfire.JobManagers; 2 | 3 | using System.Linq.Expressions; 4 | using global::Hangfire; 5 | using Mellon.MultiTenant.Base; 6 | using Mellon.MultiTenant.Base.Interfaces; 7 | using Mellon.MultiTenant.Hangfire.Interfaces; 8 | 9 | internal class MultiTenantBackgroundJobManager( 10 | IMultiTenantConfiguration multiTenantConfiguration, 11 | IBackgroundJobClient backgroundJobClient, 12 | MultiTenantSettings multiTenantSettings) : IMultiTenantBackgroundJobManager 13 | { 14 | public string Enqueue(Expression methodCall) => backgroundJobClient.Enqueue(multiTenantConfiguration.Tenant, methodCall); 15 | 16 | public string Enqueue(Expression> methodCall) => backgroundJobClient.Enqueue(multiTenantConfiguration.Tenant, methodCall); 17 | 18 | public string Enqueue(Expression> methodCall) => backgroundJobClient.Enqueue(multiTenantConfiguration.Tenant, methodCall); 19 | 20 | public string Enqueue(Expression> methodCall) => backgroundJobClient.Enqueue(multiTenantConfiguration.Tenant, methodCall); 21 | 22 | public IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression methodCall) 23 | { 24 | var data = new List<(string Tenant, string JobId)>(); 25 | 26 | foreach (var tenant in multiTenantSettings.Tenants) 27 | { 28 | data.Add((tenant, backgroundJobClient.Enqueue(tenant, methodCall))); 29 | } 30 | 31 | return data; 32 | } 33 | 34 | public IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall) 35 | { 36 | var data = new List<(string Tenant, string JobId)>(); 37 | 38 | foreach (var tenant in multiTenantSettings.Tenants) 39 | { 40 | data.Add((tenant, backgroundJobClient.Enqueue(tenant, methodCall))); 41 | } 42 | 43 | return data; 44 | } 45 | 46 | public IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall) 47 | { 48 | var data = new List<(string Tenant, string JobId)>(); 49 | 50 | foreach (var tenant in multiTenantSettings.Tenants) 51 | { 52 | data.Add((tenant, backgroundJobClient.Enqueue(tenant, methodCall))); 53 | } 54 | 55 | return data; 56 | } 57 | 58 | public IList<(string Tenant, string JobId)> EnqueueForAllTenants(Expression> methodCall) 59 | { 60 | var data = new List<(string Tenant, string JobId)>(); 61 | 62 | foreach (var tenant in multiTenantSettings.Tenants) 63 | { 64 | data.Add((tenant, backgroundJobClient.Enqueue(tenant, methodCall))); 65 | } 66 | 67 | return data; 68 | } 69 | 70 | public string Schedule(Expression methodCall, TimeSpan delay) => backgroundJobClient.Schedule(multiTenantConfiguration.Tenant, methodCall, delay); 71 | 72 | public string Schedule(Expression> methodCall, TimeSpan delay) => backgroundJobClient.Schedule(multiTenantConfiguration.Tenant, methodCall, delay); 73 | 74 | public string Schedule(Expression> methodCall, TimeSpan delay) => backgroundJobClient.Schedule(multiTenantConfiguration.Tenant, methodCall, delay); 75 | 76 | public string Schedule(Expression> methodCall, TimeSpan delay) => backgroundJobClient.Schedule(multiTenantConfiguration.Tenant, methodCall, delay); 77 | 78 | public IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression methodCall, TimeSpan delay) 79 | { 80 | var data = new List<(string Tenant, string JobId)>(); 81 | 82 | foreach (var tenant in multiTenantSettings.Tenants) 83 | { 84 | data.Add((tenant, backgroundJobClient.Schedule(tenant, methodCall, delay))); 85 | } 86 | 87 | return data; 88 | } 89 | 90 | public IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay) 91 | { 92 | var data = new List<(string Tenant, string JobId)>(); 93 | 94 | foreach (var tenant in multiTenantSettings.Tenants) 95 | { 96 | data.Add((tenant, backgroundJobClient.Schedule(tenant, methodCall, delay))); 97 | } 98 | 99 | return data; 100 | } 101 | 102 | public IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay) 103 | { 104 | var data = new List<(string Tenant, string JobId)>(); 105 | 106 | foreach (var tenant in multiTenantSettings.Tenants) 107 | { 108 | data.Add((tenant, backgroundJobClient.Schedule(tenant, methodCall, delay))); 109 | } 110 | 111 | return data; 112 | } 113 | 114 | public IList<(string Tenant, string JobId)> ScheduleForAllTenants(Expression> methodCall, TimeSpan delay) 115 | { 116 | var data = new List<(string Tenant, string JobId)>(); 117 | 118 | foreach (var tenant in multiTenantSettings.Tenants) 119 | { 120 | data.Add((tenant, backgroundJobClient.Schedule(tenant, methodCall, delay))); 121 | } 122 | 123 | return data; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Mellon.MultiTenant/Extensions/MultiTenantExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Microsoft.Extensions.DependencyInjection; 3 | #pragma warning restore IDE0130 // Namespace does not match folder structure 4 | 5 | using Mellon.MultiTenant; 6 | using Mellon.MultiTenant.Base; 7 | using Mellon.MultiTenant.Base.Interfaces; 8 | using Mellon.MultiTenant.Middlewares; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Routing; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Hosting; 15 | 16 | public static class MultiTenantExtensions 17 | { 18 | public static IServiceCollection AddMultiTenant(this IServiceCollection services, Action options) 19 | { 20 | var multiTenantOptions = new MultiTenantOptions(); 21 | 22 | options(multiTenantOptions); 23 | 24 | return services.AddMultiTenant(multiTenantOptions); 25 | } 26 | 27 | public static IServiceCollection AddMultiTenant(this IServiceCollection services) 28 | { 29 | var multiTenantOptions = new MultiTenantOptions(); 30 | 31 | return services.AddMultiTenant(multiTenantOptions); 32 | } 33 | 34 | public static IApplicationBuilder UseMultiTenant( 35 | this IApplicationBuilder builder) 36 | { 37 | builder.UseMiddleware(); 38 | 39 | if (builder is IEndpointRouteBuilder routeBuilder) 40 | { 41 | routeBuilder.AddRefreshEndpoint(); 42 | } 43 | 44 | builder.ApplicationServices.GetRequiredService(); 45 | 46 | return builder; 47 | } 48 | 49 | private static IServiceCollection AddMultiTenant( 50 | this IServiceCollection services, 51 | MultiTenantOptions multiTenantOptions) 52 | { 53 | services.AddSingleton((serviceProvider) => 54 | { 55 | var multiTenantSettings = new MultiTenantSettings(); 56 | 57 | var hostEnvironment = serviceProvider.GetRequiredService(); 58 | 59 | var configuration = serviceProvider.GetRequiredService(); 60 | 61 | var multiTenantSource = serviceProvider.GetRequiredService(); 62 | 63 | var multiTenantOptions = serviceProvider.GetRequiredService(); 64 | 65 | var tenants = MultiTenantSettings.LoadTenantsAsync(multiTenantOptions, configuration).GetAwaiter().GetResult(); 66 | 67 | if (tenants is null || tenants.Length == 0) 68 | { 69 | throw new Exception("Invalid Configuration"); 70 | } 71 | 72 | foreach (var tenant in tenants) 73 | { 74 | multiTenantSettings.LoadConfiguration( 75 | tenant, 76 | MultiTenantSettings.BuildTenantConfiguration( 77 | hostEnvironment, 78 | multiTenantSource, 79 | tenant)); 80 | } 81 | 82 | return multiTenantSettings; 83 | }); 84 | 85 | services.AddSingleton((serviceProvider) => 86 | { 87 | var configuration = serviceProvider.GetRequiredService(); 88 | 89 | configuration.GetSection("MultiTenant").Bind(multiTenantOptions); 90 | 91 | return multiTenantOptions; 92 | }); 93 | 94 | services.AddScoped(); 95 | 96 | if (multiTenantOptions.CustomMultiTenantConfigurationSource is null) 97 | { 98 | services.AddSingleton(); 99 | } 100 | else 101 | { 102 | services.AddSingleton( 103 | typeof(IMultiTenantConfigurationSource), 104 | multiTenantOptions.CustomMultiTenantConfigurationSource); 105 | } 106 | 107 | services.AddScoped(); 108 | 109 | return services; 110 | } 111 | 112 | private static IEndpointRouteBuilder AddRefreshEndpoint(this IEndpointRouteBuilder routeBuilder) 113 | { 114 | routeBuilder.MapGet("refresh-settings/{tenantName?}", RefreshEndpointAsync); 115 | 116 | routeBuilder.MapGet("refresh-settings", RefreshEndpointAsync); 117 | 118 | return routeBuilder; 119 | } 120 | 121 | private static async Task RefreshEndpointAsync( 122 | string tenantName, 123 | IConfiguration configuration, 124 | IHostEnvironment hostEnvironment, 125 | IMultiTenantConfigurationSource multiTenantSource, 126 | MultiTenantOptions multiTenantOptions, 127 | MultiTenantSettings multiTenantSettings) 128 | { 129 | bool TryFindAndRefreshSettings(string tenantName) 130 | { 131 | if (multiTenantSettings.GetConfigurations.TryGetValue(tenantName, out var conf)) 132 | { 133 | if (conf is IConfigurationRoot configurationRoot) 134 | { 135 | configurationRoot.Reload(); 136 | } 137 | 138 | return true; 139 | } 140 | 141 | return false; 142 | } 143 | 144 | if (!string.IsNullOrEmpty(tenantName)) 145 | { 146 | if (!TryFindAndRefreshSettings(tenantName)) 147 | { 148 | return Results.NotFound(new { Message = "Tenant not found" }); 149 | } 150 | } 151 | else 152 | { 153 | var tenants = await MultiTenantSettings.LoadTenantsAsync(multiTenantOptions, configuration); 154 | 155 | foreach (var tenant in tenants) 156 | { 157 | if (!TryFindAndRefreshSettings(tenant)) 158 | { 159 | multiTenantSettings.LoadConfiguration( 160 | tenant, 161 | MultiTenantSettings.BuildTenantConfiguration( 162 | hostEnvironment, 163 | multiTenantSource, 164 | tenant)); 165 | } 166 | } 167 | } 168 | 169 | return Results.Ok(new { Message = "Refresh Done!" }); 170 | } 171 | } -------------------------------------------------------------------------------- /Mellon.MultiTenant.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33205.214 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mellon.MultiTenant", "src\Mellon.MultiTenant\Mellon.MultiTenant.csproj", "{857BD408-DB4C-4F87-B24E-ECFD60D47AAB}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{722E9D04-F483-4F6D-B00F-4584C2ECF279}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{830B13DC-E58B-47BC-AF46-202841AF1B3A}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "samples\Api\WebApi.csproj", "{E68DE0F6-2253-4988-A8DC-C76FD75FBD09}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DAB5186D-DDFF-4354-8F00-DD2F694039C3}" 15 | ProjectSection(SolutionItems) = preProject 16 | .editorconfig = .editorconfig 17 | .github\workflows\buildAndPush.yml = .github\workflows\buildAndPush.yml 18 | .github\workflows\buildAndPushPR.yml = .github\workflows\buildAndPushPR.yml 19 | Directory.Build.props = Directory.Build.props 20 | Directory.Packages.props = Directory.Packages.props 21 | pubdev.ruleset = pubdev.ruleset 22 | README.md = README.md 23 | EndProjectSection 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mellon.MultiTenant.Azure", "src\Mellon.MultiTenant.Azure\Mellon.MultiTenant.Azure.csproj", "{7E18D01E-7DFD-4A66-B574-60AB2BF027EB}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mellon.MultiTenant.Base", "src\Mellon.MultiTenant.Base\Mellon.MultiTenant.Base.csproj", "{B4CF07E7-7DAC-47E9-A9F3-4244F772096F}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mellon.MultiTenant.ConfigServer", "src\Mellon.MultiTenant.ConfigServer\Mellon.MultiTenant.ConfigServer.csproj", "{9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiHangfire", "samples\Hangfire\WebApiHangfire.csproj", "{BBF26975-DFDF-4379-9778-0C6A7582A8D2}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mellon.MultiTenant.Hangfire", "src\Mellon.MultiTenant.HangFire\Mellon.MultiTenant.Hangfire.csproj", "{A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {857BD408-DB4C-4F87-B24E-ECFD60D47AAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {857BD408-DB4C-4F87-B24E-ECFD60D47AAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {857BD408-DB4C-4F87-B24E-ECFD60D47AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {857BD408-DB4C-4F87-B24E-ECFD60D47AAB}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {E68DE0F6-2253-4988-A8DC-C76FD75FBD09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {E68DE0F6-2253-4988-A8DC-C76FD75FBD09}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {E68DE0F6-2253-4988-A8DC-C76FD75FBD09}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {E68DE0F6-2253-4988-A8DC-C76FD75FBD09}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {7E18D01E-7DFD-4A66-B574-60AB2BF027EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {7E18D01E-7DFD-4A66-B574-60AB2BF027EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {7E18D01E-7DFD-4A66-B574-60AB2BF027EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {7E18D01E-7DFD-4A66-B574-60AB2BF027EB}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {B4CF07E7-7DAC-47E9-A9F3-4244F772096F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {B4CF07E7-7DAC-47E9-A9F3-4244F772096F}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {B4CF07E7-7DAC-47E9-A9F3-4244F772096F}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {B4CF07E7-7DAC-47E9-A9F3-4244F772096F}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {BBF26975-DFDF-4379-9778-0C6A7582A8D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {BBF26975-DFDF-4379-9778-0C6A7582A8D2}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {BBF26975-DFDF-4379-9778-0C6A7582A8D2}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {BBF26975-DFDF-4379-9778-0C6A7582A8D2}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696}.Release|Any CPU.Build.0 = Release|Any CPU 69 | EndGlobalSection 70 | GlobalSection(SolutionProperties) = preSolution 71 | HideSolutionNode = FALSE 72 | EndGlobalSection 73 | GlobalSection(NestedProjects) = preSolution 74 | {857BD408-DB4C-4F87-B24E-ECFD60D47AAB} = {722E9D04-F483-4F6D-B00F-4584C2ECF279} 75 | {E68DE0F6-2253-4988-A8DC-C76FD75FBD09} = {830B13DC-E58B-47BC-AF46-202841AF1B3A} 76 | {7E18D01E-7DFD-4A66-B574-60AB2BF027EB} = {722E9D04-F483-4F6D-B00F-4584C2ECF279} 77 | {B4CF07E7-7DAC-47E9-A9F3-4244F772096F} = {722E9D04-F483-4F6D-B00F-4584C2ECF279} 78 | {9A1BA7B3-63BD-4E64-A8A7-321F0AFA0F5C} = {722E9D04-F483-4F6D-B00F-4584C2ECF279} 79 | {BBF26975-DFDF-4379-9778-0C6A7582A8D2} = {830B13DC-E58B-47BC-AF46-202841AF1B3A} 80 | {A62FA3DB-D0E9-4A94-8FAC-EAE53FD6F696} = {722E9D04-F483-4F6D-B00F-4584C2ECF279} 81 | EndGlobalSection 82 | GlobalSection(ExtensibilityGlobals) = postSolution 83 | SolutionGuid = {12CA4757-DB3B-48BB-AE88-B68805FE0479} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /.github/workflows/buildAndPush.yml: -------------------------------------------------------------------------------- 1 | name: Build, Pack and Publish 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "**/README.md" 8 | - "**/samples/*" 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | main: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: "0" 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 8.0.x 24 | 25 | - name: Install GitVersion 26 | uses: gittools/actions/gitversion/setup@v0.9.7 27 | with: 28 | versionSpec: "6.0.0-alpha.1" 29 | includePrerelease: true 30 | 31 | - name: Determine Version 32 | id: gitversion 33 | uses: gittools/actions/gitversion/execute@v0.9.7 34 | 35 | - name: Display GitVersion outputs 36 | run: | 37 | echo "Major: ${{ steps.gitversion.outputs.major }}" 38 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 39 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 40 | 41 | - name: Restore dependencies 42 | run: dotnet restore ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj 43 | 44 | - name: Build 45 | run: dotnet build ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj --no-restore 46 | 47 | - name: Create the package 48 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj -p:Version=3.0.${{ github.run_number }} 49 | 50 | - name: Publish the package to nuget.org 51 | run: dotnet nuget push ./src/Mellon.MultiTenant/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 52 | env: 53 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 54 | azure: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | with: 59 | fetch-depth: "0" 60 | 61 | - name: Setup .NET 62 | uses: actions/setup-dotnet@v1 63 | with: 64 | dotnet-version: 8.0.x 65 | 66 | - name: Install GitVersion 67 | uses: gittools/actions/gitversion/setup@v0.9.7 68 | with: 69 | versionSpec: "6.0.0-alpha.1" 70 | includePrerelease: true 71 | 72 | - name: Determine Version 73 | id: gitversion 74 | uses: gittools/actions/gitversion/execute@v0.9.7 75 | 76 | - name: Display GitVersion outputs 77 | run: | 78 | echo "Major: ${{ steps.gitversion.outputs.major }}" 79 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 80 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 81 | 82 | - name: Restore dependencies 83 | run: dotnet restore ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj 84 | 85 | - name: Build 86 | run: dotnet build ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj --no-restore 87 | 88 | - name: Create the package 89 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj -p:Version=3.0.${{ github.run_number }} 90 | 91 | - name: Publish the package to nuget.org 92 | run: dotnet nuget push ./src/Mellon.MultiTenant.Azure/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 93 | env: 94 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 95 | config-server: 96 | runs-on: ubuntu-latest 97 | steps: 98 | - uses: actions/checkout@v2 99 | with: 100 | fetch-depth: "0" 101 | 102 | - name: Setup .NET 103 | uses: actions/setup-dotnet@v1 104 | with: 105 | dotnet-version: 8.0.x 106 | 107 | - name: Install GitVersion 108 | uses: gittools/actions/gitversion/setup@v0.9.7 109 | with: 110 | versionSpec: "6.0.0-alpha.1" 111 | includePrerelease: true 112 | 113 | - name: Determine Version 114 | id: gitversion 115 | uses: gittools/actions/gitversion/execute@v0.9.7 116 | 117 | - name: Display GitVersion outputs 118 | run: | 119 | echo "Major: ${{ steps.gitversion.outputs.major }}" 120 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 121 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 122 | 123 | - name: Restore dependencies 124 | run: dotnet restore ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj 125 | 126 | - name: Build 127 | run: dotnet build ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj --no-restore 128 | 129 | - name: Create the package 130 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj -p:Version=3.0.${{ github.run_number }} 131 | 132 | - name: Publish the package to nuget.org 133 | run: dotnet nuget push ./src/Mellon.MultiTenant.ConfigServer/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 134 | env: 135 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 136 | base: 137 | runs-on: ubuntu-latest 138 | steps: 139 | - uses: actions/checkout@v2 140 | with: 141 | fetch-depth: "0" 142 | 143 | - name: Setup .NET 144 | uses: actions/setup-dotnet@v1 145 | with: 146 | dotnet-version: 8.0.x 147 | 148 | - name: Install GitVersion 149 | uses: gittools/actions/gitversion/setup@v0.9.7 150 | with: 151 | versionSpec: "6.0.0-alpha.1" 152 | includePrerelease: true 153 | 154 | - name: Determine Version 155 | id: gitversion 156 | uses: gittools/actions/gitversion/execute@v0.9.7 157 | 158 | - name: Display GitVersion outputs 159 | run: | 160 | echo "Major: ${{ steps.gitversion.outputs.major }}" 161 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 162 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 163 | 164 | - name: Restore dependencies 165 | run: dotnet restore ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj 166 | 167 | - name: Build 168 | run: dotnet build ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj --no-restore 169 | 170 | - name: Create the package 171 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj -p:Version=3.0.${{ github.run_number }} 172 | 173 | - name: Publish the package to nuget.org 174 | run: dotnet nuget push ./src/Mellon.MultiTenant.Base/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 175 | env: 176 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 177 | hangfire: 178 | runs-on: ubuntu-latest 179 | steps: 180 | - uses: actions/checkout@v2 181 | with: 182 | fetch-depth: "0" 183 | 184 | - name: Setup .NET 185 | uses: actions/setup-dotnet@v1 186 | with: 187 | dotnet-version: 8.0.x 188 | 189 | - name: Install GitVersion 190 | uses: gittools/actions/gitversion/setup@v0.9.7 191 | with: 192 | versionSpec: "6.0.0-alpha.1" 193 | includePrerelease: true 194 | 195 | - name: Determine Version 196 | id: gitversion 197 | uses: gittools/actions/gitversion/execute@v0.9.7 198 | 199 | - name: Display GitVersion outputs 200 | run: | 201 | echo "Major: ${{ steps.gitversion.outputs.major }}" 202 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 203 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 204 | 205 | - name: Restore dependencies 206 | run: dotnet restore ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj 207 | 208 | - name: Build 209 | run: dotnet build ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj --no-restore 210 | 211 | - name: Create the package 212 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj -p:Version=3.0.${{ github.run_number }} 213 | 214 | - name: Publish the package to nuget.org 215 | run: dotnet nuget push ./src/Mellon.MultiTenant.Hangfire/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 216 | env: 217 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 218 | -------------------------------------------------------------------------------- /.github/workflows/buildAndPushPR.yml: -------------------------------------------------------------------------------- 1 | name: Build, Pack and Publish ALPHA 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths-ignore: 7 | - "**/README.md" 8 | - "**/samples/*" 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | main: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: "0" 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 8.0.x 24 | 25 | - name: Install GitVersion 26 | uses: gittools/actions/gitversion/setup@v0.9.7 27 | with: 28 | versionSpec: "6.0.0-alpha.1" 29 | includePrerelease: true 30 | 31 | - name: Determine Version 32 | id: gitversion 33 | uses: gittools/actions/gitversion/execute@v0.9.7 34 | 35 | - name: Display GitVersion outputs 36 | run: | 37 | echo "Major: ${{ steps.gitversion.outputs.major }}" 38 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 39 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 40 | 41 | - name: Restore dependencies 42 | run: dotnet restore ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj 43 | 44 | - name: Build 45 | run: dotnet build ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj --no-restore 46 | 47 | - name: Create the package 48 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant/Mellon.MultiTenant.csproj -p:Version=3.0.${{ github.run_number }}-alpha 49 | 50 | - name: Publish the package to nuget.org 51 | run: dotnet nuget push ./src/Mellon.MultiTenant/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 52 | env: 53 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 54 | azure: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | with: 59 | fetch-depth: "0" 60 | 61 | - name: Setup .NET 62 | uses: actions/setup-dotnet@v1 63 | with: 64 | dotnet-version: 8.0.x 65 | 66 | - name: Install GitVersion 67 | uses: gittools/actions/gitversion/setup@v0.9.7 68 | with: 69 | versionSpec: "6.0.0-alpha.1" 70 | includePrerelease: true 71 | 72 | - name: Determine Version 73 | id: gitversion 74 | uses: gittools/actions/gitversion/execute@v0.9.7 75 | 76 | - name: Display GitVersion outputs 77 | run: | 78 | echo "Major: ${{ steps.gitversion.outputs.major }}" 79 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 80 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 81 | 82 | - name: Restore dependencies 83 | run: dotnet restore ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj 84 | 85 | - name: Build 86 | run: dotnet build ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj --no-restore 87 | 88 | - name: Create the package 89 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Azure/Mellon.MultiTenant.Azure.csproj -p:Version=3.0.${{ github.run_number }}-alpha 90 | 91 | - name: Publish the package to nuget.org 92 | run: dotnet nuget push ./src/Mellon.MultiTenant.Azure/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 93 | env: 94 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 95 | config-server: 96 | runs-on: ubuntu-latest 97 | steps: 98 | - uses: actions/checkout@v2 99 | with: 100 | fetch-depth: "0" 101 | 102 | - name: Setup .NET 103 | uses: actions/setup-dotnet@v1 104 | with: 105 | dotnet-version: 8.0.x 106 | 107 | - name: Install GitVersion 108 | uses: gittools/actions/gitversion/setup@v0.9.7 109 | with: 110 | versionSpec: "6.0.0-alpha.1" 111 | includePrerelease: true 112 | 113 | - name: Determine Version 114 | id: gitversion 115 | uses: gittools/actions/gitversion/execute@v0.9.7 116 | 117 | - name: Display GitVersion outputs 118 | run: | 119 | echo "Major: ${{ steps.gitversion.outputs.major }}" 120 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 121 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 122 | 123 | - name: Restore dependencies 124 | run: dotnet restore ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj 125 | 126 | - name: Build 127 | run: dotnet build ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj --no-restore 128 | 129 | - name: Create the package 130 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.ConfigServer/Mellon.MultiTenant.ConfigServer.csproj -p:Version=3.0.${{ github.run_number }}-alpha 131 | 132 | - name: Publish the package to nuget.org 133 | run: dotnet nuget push ./src/Mellon.MultiTenant.ConfigServer/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 134 | env: 135 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 136 | base: 137 | runs-on: ubuntu-latest 138 | steps: 139 | - uses: actions/checkout@v2 140 | with: 141 | fetch-depth: "0" 142 | 143 | - name: Setup .NET 144 | uses: actions/setup-dotnet@v1 145 | with: 146 | dotnet-version: 8.0.x 147 | 148 | - name: Install GitVersion 149 | uses: gittools/actions/gitversion/setup@v0.9.7 150 | with: 151 | versionSpec: "6.0.0-alpha.1" 152 | includePrerelease: true 153 | 154 | - name: Determine Version 155 | id: gitversion 156 | uses: gittools/actions/gitversion/execute@v0.9.7 157 | 158 | - name: Display GitVersion outputs 159 | run: | 160 | echo "Major: ${{ steps.gitversion.outputs.major }}" 161 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 162 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 163 | 164 | - name: Restore dependencies 165 | run: dotnet restore ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj 166 | 167 | - name: Build 168 | run: dotnet build ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj --no-restore 169 | 170 | - name: Create the package 171 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Base/Mellon.MultiTenant.Base.csproj -p:Version=3.0.${{ github.run_number }}-alpha 172 | 173 | - name: Publish the package to nuget.org 174 | run: dotnet nuget push ./src/Mellon.MultiTenant.Base/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 175 | env: 176 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} 177 | hangfire: 178 | runs-on: ubuntu-latest 179 | steps: 180 | - uses: actions/checkout@v2 181 | with: 182 | fetch-depth: "0" 183 | 184 | - name: Setup .NET 185 | uses: actions/setup-dotnet@v1 186 | with: 187 | dotnet-version: 8.0.x 188 | 189 | - name: Install GitVersion 190 | uses: gittools/actions/gitversion/setup@v0.9.7 191 | with: 192 | versionSpec: "6.0.0-alpha.1" 193 | includePrerelease: true 194 | 195 | - name: Determine Version 196 | id: gitversion 197 | uses: gittools/actions/gitversion/execute@v0.9.7 198 | 199 | - name: Display GitVersion outputs 200 | run: | 201 | echo "Major: ${{ steps.gitversion.outputs.major }}" 202 | echo "Minor: ${{ steps.gitversion.outputs.minor }}" 203 | echo "Patch: ${{ steps.gitversion.outputs.patch }}" 204 | 205 | - name: Restore dependencies 206 | run: dotnet restore ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj 207 | 208 | - name: Build 209 | run: dotnet build ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj --no-restore 210 | 211 | - name: Create the package 212 | run: dotnet pack --configuration Release ./src/Mellon.MultiTenant.Hangfire/Mellon.MultiTenant.Hangfire.csproj -p:Version=3.0.${{ github.run_number }}-alpha 213 | 214 | - name: Publish the package to nuget.org 215 | run: dotnet nuget push ./src/Mellon.MultiTenant.Hangfire/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 216 | env: 217 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_KEY }} -------------------------------------------------------------------------------- /pubdev.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = crlf 5 | insert_final_newline = false 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | 9 | # CSharp code style settings: 10 | [*.cs] 11 | # Prefer "var" everywhere 12 | csharp_style_var_for_built_in_types = true:error 13 | csharp_style_var_when_type_is_apparent = true:suggestion 14 | csharp_style_var_elsewhere = true:suggestion 15 | 16 | # Prefer method-like constructs to have a block body 17 | csharp_style_expression_bodied_methods = true:error 18 | csharp_style_expression_bodied_constructors = false:warning 19 | csharp_style_expression_bodied_operators = false:warning 20 | 21 | # Prefer property-like constructs to have an expression-body 22 | csharp_style_expression_bodied_properties = true:warning 23 | csharp_style_expression_bodied_indexers = true:warning 24 | csharp_style_expression_bodied_accessors = true:warning 25 | 26 | # Suggest more modern language features when available 27 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 28 | csharp_style_pattern_matching_over_as_with_null_check = true:error 29 | csharp_style_inlined_variable_declaration = true:error 30 | csharp_style_throw_expression = true:silent 31 | csharp_style_conditional_delegate_call = true:error 32 | 33 | # Newline settings 34 | csharp_new_line_before_open_brace = all 35 | csharp_new_line_before_else = true 36 | csharp_new_line_before_catch = true 37 | csharp_new_line_before_finally = true 38 | csharp_new_line_before_members_in_object_initializers = true 39 | csharp_new_line_before_members_in_anonymous_types = true 40 | csharp_indent_labels = one_less_than_current 41 | csharp_space_around_binary_operators = before_and_after 42 | csharp_using_directive_placement = inside_namespace:error 43 | csharp_prefer_simple_using_statement = true:warning 44 | csharp_prefer_braces = true:error 45 | csharp_style_namespace_declarations = file_scoped:error 46 | csharp_style_prefer_method_group_conversion = true:silent 47 | csharp_style_prefer_top_level_statements = true:silent 48 | csharp_style_expression_bodied_lambdas = true:warning 49 | csharp_style_expression_bodied_local_functions = false:warning 50 | csharp_style_prefer_null_check_over_type_check = true:warning 51 | csharp_prefer_simple_default_expression = true:error 52 | csharp_style_prefer_local_over_anonymous_function = true:warning 53 | csharp_style_prefer_index_operator = true:warning 54 | csharp_style_prefer_range_operator = true:warning 55 | csharp_style_implicit_object_creation_when_type_is_apparent = true:error 56 | csharp_style_prefer_tuple_swap = true:error 57 | csharp_style_prefer_utf8_string_literals = true:silent 58 | csharp_style_deconstructed_variable_declaration = true:silent 59 | csharp_style_unused_value_assignment_preference = discard_variable:silent 60 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 61 | csharp_prefer_static_local_function = true:warning 62 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent 63 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent 64 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent 65 | csharp_style_prefer_switch_expression = true:error 66 | csharp_style_prefer_pattern_matching = true:warning 67 | csharp_style_prefer_not_pattern = true:error 68 | csharp_style_prefer_extended_property_pattern = true:error 69 | csharp_space_after_keywords_in_control_flow_statements = true 70 | dotnet_diagnostic.CA1070.severity = suggestion 71 | dotnet_diagnostic.IDE0290.severity = none 72 | csharp_style_prefer_primary_constructors = true:suggestion 73 | csharp_style_prefer_readonly_struct_member = true:suggestion 74 | csharp_style_prefer_readonly_struct = true:suggestion 75 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent 76 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent 77 | 78 | # Dotnet code style settings: 79 | [*.{cs,vb}] 80 | # Sort using and Import directives with System.* appearing first 81 | dotnet_sort_system_directives_first = true 82 | # Avoid "this." and "Me." if not necessary 83 | dotnet_style_qualification_for_field = false:error 84 | dotnet_style_qualification_for_property = false:error 85 | dotnet_style_qualification_for_method = false:error 86 | dotnet_style_qualification_for_event = false:error 87 | 88 | # Use language keywords instead of framework type names for type references 89 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 90 | dotnet_style_predefined_type_for_member_access = true:error 91 | 92 | # Suggest more modern language features when available 93 | dotnet_style_object_initializer = true:error 94 | dotnet_style_collection_initializer = true:error 95 | dotnet_style_coalesce_expression = true:error 96 | dotnet_style_null_propagation = true:error 97 | dotnet_style_explicit_tuple_names = true:error 98 | 99 | # Casing Options 100 | 101 | [*.{cs,vb}] 102 | #### Naming styles #### 103 | 104 | # Naming rules 105 | 106 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = error 107 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 108 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 109 | 110 | dotnet_naming_rule.types_should_be_pascal_case.severity = error 111 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 112 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 113 | 114 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error 115 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 116 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 117 | 118 | # Symbol specifications 119 | 120 | dotnet_naming_symbols.interface.applicable_kinds = interface 121 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 122 | dotnet_naming_symbols.interface.required_modifiers = 123 | 124 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 125 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 126 | dotnet_naming_symbols.types.required_modifiers = 127 | 128 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 129 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 130 | dotnet_naming_symbols.non_field_members.required_modifiers = 131 | 132 | # Naming styles 133 | 134 | dotnet_naming_style.begins_with_i.required_prefix = I 135 | dotnet_naming_style.begins_with_i.required_suffix = 136 | dotnet_naming_style.begins_with_i.word_separator = 137 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 138 | 139 | dotnet_naming_style.pascal_case.required_prefix = 140 | dotnet_naming_style.pascal_case.required_suffix = 141 | dotnet_naming_style.pascal_case.word_separator = 142 | dotnet_naming_style.pascal_case.capitalization = pascal_case 143 | 144 | # Constants are UPPERCASE 145 | dotnet_naming_rule.constants_should_be_upper_case.severity = error 146 | dotnet_naming_rule.constants_should_be_upper_case.symbols = constants 147 | dotnet_naming_rule.constants_should_be_upper_case.style = constant_style 148 | 149 | dotnet_naming_symbols.constants.applicable_kinds = field, local 150 | dotnet_naming_symbols.constants.required_modifiers = const 151 | 152 | dotnet_naming_style.constant_style.capitalization = all_upper 153 | 154 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 155 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 156 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 157 | 158 | dotnet_naming_symbols.private_fields.applicable_kinds = field 159 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 160 | 161 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 162 | dotnet_naming_style.prefix_underscore.required_prefix = _ 163 | 164 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 165 | tab_width = 4 166 | indent_size = 4 167 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error 168 | dotnet_style_prefer_auto_properties = true:error 169 | dotnet_style_prefer_simplified_boolean_expressions = true:error 170 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 171 | dotnet_style_prefer_conditional_expression_over_return = true:silent 172 | dotnet_style_prefer_inferred_tuple_names = true:warning 173 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning 174 | dotnet_style_prefer_compound_assignment = true:error 175 | dotnet_style_prefer_simplified_interpolation = true:error 176 | dotnet_style_namespace_match_folder = true:error 177 | dotnet_style_readonly_field = true:error 178 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:error 179 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent 180 | dotnet_style_allow_multiple_blank_lines_experimental = false:silent 181 | dotnet_code_quality_unused_parameters = all:error 182 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 183 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 184 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 185 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion 186 | dotnet_style_prefer_collection_expression = when_types_exactly_match:suggestion 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] [![LinkedIn][linkedin-shield]][linkedin-url] [![LinkedIn][linkedin-shield]][linkedin2-url] 2 | 3 | ## Mellon.MultiTenant by [@PubDev](https://www.youtube.com/@PubDev) 4 | 5 | | Package | Version | Alpha | 6 | | :---------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------: | 7 | | **Mellon.MultiTenant** | [![Nuget](https://img.shields.io/nuget/v/Mellon-MultiTenant)](https://www.nuget.org/packages/Mellon-MultiTenant) | [![Nuget](https://img.shields.io/nuget/vpre/Mellon-MultiTenant)](https://www.nuget.org/packages/Mellon-MultiTenant) | 8 | | **Mellon.MultiTenant.Base** | [![Nuget](https://img.shields.io/nuget/v/Mellon-MultiTenant-Base)](https://www.nuget.org/packages/Mellon-MultiTenant-Base) | [![Nuget](https://img.shields.io/nuget/vpre/Mellon-MultiTenant-Base)](https://www.nuget.org/packages/Mellon-MultiTenant-Base) | 9 | | **Mellon.MultiTenant.ConfigServer** | [![Nuget](https://img.shields.io/nuget/v/Mellon-MultiTenant-ConfigServer)](https://www.nuget.org/packages/Mellon-MultiTenant-ConfigServer) | [![Nuget](https://img.shields.io/nuget/vpre/Mellon-MultiTenant-ConfigServer)](https://www.nuget.org/packages/Mellon-MultiTenant-ConfigServer) | 10 | | **Mellon.MultiTenant.Azure** | [![Nuget](https://img.shields.io/nuget/v/Mellon-MultiTenant-Azure)](https://www.nuget.org/packages/Mellon-MultiTenant-Azure) | [![Nuget](https://img.shields.io/nuget/vpre/Mellon-MultiTenant-Azure)](https://www.nuget.org/packages/Mellon-MultiTenant-Azure) | 11 | | **Mellon.MultiTenant.Hangfire** | [![Nuget](https://img.shields.io/nuget/v/Mellon-MultiTenant-Hangfire)](https://www.nuget.org/packages/Mellon-MultiTenant-Hangfire) | [![Nuget](https://img.shields.io/nuget/vpre/Mellon-MultiTenant-Hangfire)](https://www.nuget.org/packages/Mellon-MultiTenant-Hangfire) | 12 | 13 | ![Downloads](https://img.shields.io/nuget/dt/Mellon-MultiTenant.svg 'Downloads') [![GitHublicense](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/1bberto/Mellon.MultiTenant/main/LICENSE) [![CI](https://github.com/Pub-Dev/Mellon.MultiTenant/actions/workflows/buildAndPush.yml/badge.svg?branch=main)](https://github.com/Pub-Dev/Mellon.MultiTenant/actions/workflows/buildAndPush.yml) 14 | 15 | Why Mellon, mellon is the Sindarin (and Noldorin) word for "friend", yes I'm a big fan of LoR, so let's be friends? 16 | 17 | ## About The Project 18 | 19 | This library was created to supply a set of tools to enable the creation of multi-tenant applications using .net. 20 | 21 | ### Built With 22 | 23 | - [net8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 24 | - [steeltoe](https://docs.steeltoe.io/api/v3/configuration) 25 | - [Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) 26 | - [Spring Cloud Config](https://cloud.spring.io/spring-cloud-config/reference/html) 27 | - The most important, Love ❤️ 28 | 29 | ## Getting Started 30 | 31 | ## Installation 32 | 33 | With package Manager: 34 | 35 | ``` 36 | Install-Package Mellon.MultiTenant 37 | ``` 38 | 39 | With .NET CLI: 40 | 41 | ``` 42 | dotnet add package Mellon.MultiTenant 43 | ``` 44 | 45 | ### Configurations 46 | 47 | There are two ways to configure the settings, via config and through the api 48 | 49 | #### Settings 50 | 51 | ```json 52 | "MultiTenant": { 53 | "ApplicationName": "customer-api", 54 | "HttpHeaderKey": "x-tenant-name", 55 | "CookieKey": "tenant-name", 56 | "QueryStringKey": "tenant-name", 57 | "TenantSource": "Settings", 58 | "SkipTenantCheckPaths": ["^/swagger.*"], 59 | "Tenants": [ 60 | "client-a", 61 | "client-b", 62 | "client-c" 63 | ] 64 | } 65 | ``` 66 | 67 | | Property | Description | Default | 68 | | :------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------: | 69 | | ApplicationName | Application name | IHostEnvironment.ApplicationName | 70 | | HttpHeaderKey | HTTP Header key, where the tenant name will be passed | `null` | 71 | | CookieKey | HTTP Cookie key, where the tenant name will be passed | `null` | 72 | | QueryStringKey | HTTP Query String key, where the tenant name will be passed | `null` | 73 | | TenantSource | Where the list of possible tenants will be stored, it can be from two sources: `Settings` or `EnvironmentVariables` | `Settings` | 74 | | Tenants | When the property `TenantSource` is set to `Settings` this property must contain the list of tenants | `null` | 75 | | 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` | 76 | | SkipTenantCheckPaths | Endpoints which the tenant do not need to be identified, for example: Swagger endpoints, you can use a regex string `^/swagger.*` | `null` | 77 | 78 | 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 `,` 79 | for example: 80 | 81 | ``` 82 | $Env:MULTITENANT_TENANTS = 'client-a,client-b,client-c' 83 | ``` 84 | 85 | ### Using the API 86 | 87 | You can also set the settings using these options while you are adding the services 88 | 89 | ```csharp 90 | builder.Services 91 | .AddMultiTenant(options => 92 | options 93 | .WithApplicationName("customer-api") 94 | .WithHttpHeader("x-tenant-name") 95 | .WithCookie("tenant-name") 96 | .WithQueryString("tenant-name") 97 | .WithDefaultTenant("client-a") 98 | .LoadFromSettings() 99 | ); 100 | ``` 101 | 102 | #### `WithApplicationName(string)` 103 | 104 | - Set the application name 105 | 106 | #### `WithHttpHeader(string)` 107 | 108 | - Set the HTTP Header key, where the tenant name will be passed 109 | 110 | #### `WithCookie(string)` 111 | 112 | - Set the HTTP Cookie key, where the tenant name will be passed 113 | 114 | #### `WithQueryString(string)` 115 | 116 | - Set the HTTP Query String key, where the tenant name will be passed 117 | 118 | #### `WithDefaultTenant(string)` 119 | 120 | - 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 😉👍 121 | 122 | ### `WithSkipTenantCheckPaths(string)` 123 | 124 | - Add a path that will be skipped during the tenant identification 125 | 126 | ### `WithSkipTenantCheckPaths(params string[])` 127 | 128 | - Add paths that will be skipped during the tenant identification 129 | 130 | #### `LoadFromSettings` 131 | 132 | - Set for when the tenant list will be loaded from the settings **MultiTenant:Tenants** 133 | 134 | #### `LoadFromEnvironmentVariable` 135 | 136 | - Set for when the tenant list will be loaded from the environment variable **MULTITENANT_TENANTS** 137 | 138 | #### `LoadFromEndpoint(Func func)` and `LoadFromEndpoint(Func func)` 139 | 140 | - 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 141 | 142 | ```json 143 | "MultiTenant": { 144 | // other settings... 145 | "Endpoint": { 146 | "Url": "[endpoint]", 147 | "Method": "GET", 148 | "Authorization": "Basic $user $password" 149 | }, 150 | // other settings... 151 | } 152 | ``` 153 | 154 | If the endpoint has authorization you can set the credencials on the property `Authorization` 155 | 156 | You can pass the Func and do what you see fit when getting the list of tenants 157 | 158 | Example: 159 | 160 | ```csharp 161 | services 162 | .AddMultiTenant(options => options.LoadFromEndpoint((endpointOptions, configuration) => 163 | { 164 | var request = new HttpRequestMessage() 165 | { 166 | RequestUri = new Uri(endpointOptions.Url), 167 | Method = new HttpMethod(endpointOptions.Method), 168 | }; 169 | 170 | if (!string.IsNullOrEmpty(endpointOptions.Authorization)) 171 | { 172 | request.Headers.Add("Authorization", endpointOptions.Authorization); 173 | } 174 | 175 | using (var client = new HttpClient()) 176 | { 177 | var result = client.Send(request); 178 | 179 | if (result.IsSuccessStatusCode) 180 | { 181 | var data = result.Content.ReadFromJsonAsync>().GetAwaiter().GetResult(); 182 | 183 | return data!.Select(x => x.Id).ToArray(); 184 | } 185 | else 186 | { 187 | var statusCode = result.StatusCode; 188 | 189 | var reason = result.ReasonPhrase; 190 | 191 | var content = result.Content.ReadAsStringAsync().GetAwaiter().GetResult(); 192 | 193 | throw new Exception($@"Error to load tenants from the url {endpointOptions.Url} StatusCode: {statusCode} Reason: {reason} Content: {content}"); 194 | } 195 | } 196 | })); 197 | ``` 198 | 199 | 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` 200 | 201 | Example: 202 | 203 | ```csharp 204 | services 205 | .AddMultiTenant(options => options.LoadFromEndpoint(x => x.TenantId)); 206 | ``` 207 | 208 | #### `WithHttpContextLoad(Func func)` 209 | 210 | - When all the possibilities above do not meet your need you can create a custom "Middleware" to identify the tenant based on a `HttpContext` 211 | 212 | #### `WithCustomTenantConfigurationSource()` 213 | 214 | - `T` must be an implementation of the interface `ITenantConfigurationSource` 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: 215 | 216 | ```csharp 217 | public class LocalXmlTenantSource : ITenantConfigurationSource 218 | { 219 | private readonly IHostEnvironment _hostEnvironment; 220 | 221 | public LocalTenantSource( 222 | IHostEnvironment hostEnvironment) 223 | { 224 | _hostEnvironment = hostEnvironment; 225 | } 226 | 227 | public IConfigurationBuilder AddSource( 228 | string tenant, 229 | IConfigurationBuilder builder) 230 | { 231 | builder.AddXmlFile($"appsettings.{tenant}.xml", true); 232 | builder.AddXmlFile($"appsettings.{tenant}.{_hostEnvironment.EnvironmentName}.xml", true); 233 | 234 | return builder; 235 | } 236 | } 237 | ``` 238 | 239 | ### Local 240 | 241 | 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: 242 | 243 | - `appsettings.{tenant}.json` 244 | - `appsettings.{tenant}.{_hostEnvironment.EnvironmentName}.json` 245 | 246 | It is also worth mentioning that the configurations will also contain: 247 | 248 | - `appsettings.json` 249 | - `appsettings.[environment].json` 250 | - `environment variables` 251 | 252 | ### Spring Cloud Config 253 | 254 | You can also load the settings from a **Spring Cloud Config Server**! 255 | 256 | To enable the usage you need to install an extra package: 257 | 258 | With package Manager: 259 | 260 | ``` 261 | Install-Package Mellon.MultiTenant.ConfigServer 262 | ``` 263 | 264 | With .NET CLI: 265 | 266 | ``` 267 | dotnet add package Mellon.MultiTenant.ConfigServer 268 | ``` 269 | 270 | Once the package is installed you need to configure its services 271 | 272 | ```csharp 273 | builder.Services 274 | .AddMultiTenant() 275 | .AddMultiTenantSpringCloudConfig(); 276 | ``` 277 | 278 | To setup [Spring Cloud Config](https://cloud.spring.io/spring-cloud-config/reference/html) on your environment check this reporitory [DotNet-ConfigServer](https://github.com/Pub-Dev/Lesson-DotNet-ConfigServer) 279 | 280 | The application name for spring cloud config will be based on the settings _**MultiTenant.ApplicationName**_ and the label will be tenant name. 281 | 282 | Example: 283 | _customer-api-client-a.yaml_ 284 | 285 | being: 286 | 287 | - _customer-api_ the application name 288 | - _client-a_ the tenant name 289 | 290 | Moreover, it is worth mentioning that the settings for each customer will also have the settings of the current files: 291 | 292 | - appsettings.json 293 | - appsettings.[environment].json 294 | - environment variables 295 | 296 | ### Azure App Configuration 297 | 298 | You can also use it as a source of configuration the **Azure App Configuration** 299 | 300 | With package Manager: 301 | 302 | ``` 303 | Install-Package Mellon.MultiTenant.Azure 304 | ``` 305 | 306 | With .NET CLI: 307 | 308 | ``` 309 | dotnet add package Mellon.MultiTenant.Azure 310 | ``` 311 | 312 | Once the package is installed you need to configure its services 313 | 314 | ```csharp 315 | builder.Services 316 | .AddMultiTenant() 317 | .AddMultiTenantAzureAppConfiguration(); 318 | ``` 319 | 320 | #### `AddMultiTenantAzureAppConfiguration(Action action = null)` 321 | 322 | if the action is not passed, the connection string used to connect on azure will be loaded from `AzureAppConfigurationConnectionString` 323 | 324 | 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>`, 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`. 325 | For example: 326 | 327 | ```csharp 328 | builder.Services 329 | .AddMultiTenant() 330 | .AddMultiTenantAzureAppConfiguration(options => 331 | options.AzureAppConfigurationOptions = (serviceProvider, tenant) => 332 | { 333 | var configuration = serviceProvider.GetRequiredService(); 334 | 335 | return azureOptions => azureOptions 336 | .Connect(configuration["AzureAppConfigurationConnectionString"]) 337 | .Select("*", tenant); 338 | } 339 | ); 340 | ``` 341 | 342 | ## Hangfire 343 | 344 | If you need to work with jobs with the concept of multi-tenant using **Hangfire** 345 | 346 | To enable the usage you need to install an extra package: 347 | 348 | With package Manager: 349 | 350 | ``` 351 | Install-Package Mellon.MultiTenant.Hangfire 352 | ``` 353 | 354 | With .NET CLI: 355 | 356 | ``` 357 | dotnet add package Mellon.MultiTenant.Hangfire 358 | ``` 359 | 360 | Once the package is installed you need to configure its services 361 | 362 | ```csharp 363 | builder.Services 364 | .AddMultiTenant() 365 | .AddMultiTenantHangfire(); 366 | ``` 367 | 368 | And when adding the Service `AddHangfire` you need to call the method `UseMultiTenant` passing the `IServiceProvider` 369 | 370 | ```csharp 371 | builder.Services.AddHangfire((serviceProvider, config) => 372 | { 373 | // some code 374 | config.UseMultiTenant(serviceProvider); 375 | // some code 376 | }); 377 | ``` 378 | 379 | To create the Worker, you need to pass the queues with the tenant names 380 | 381 | ```csharp 382 | builder.Services.AddHangfireServer((serviceProvider, config) => 383 | { 384 | var multiTenantSettings = serviceProvider.GetRequiredService(); 385 | 386 | var queues = new List(multiTenantSettings.Tenants); 387 | 388 | // if you want to add more queues 389 | queues.Add("cron"); 390 | queues.Add("default"); 391 | config.Queues = tenants.ToArray(); 392 | 393 | // some code 394 | }); 395 | ``` 396 | 397 | For **ScheduleJobs** and **BackgroundJob** the queue name will be the _tenant name_ 398 | 399 | For **RecurringJobs** the default queue could vary from job to job 400 | 401 | ### RecurringJobs 🗓️ 402 | 403 | To create Recurring Jobs you just need to use the interface `IMultiTenantRecurringJobManager`, this interface will have the following methods and extension methods: 404 | 405 | #### `AddOrUpdateForAllTenants(string recurringJobId,Expression> methodCall, string cronExpression, TimeZoneInfo timeZone = null,string queue = "default")` 406 | 407 | It will create a recurring job from the type `T.Method` for all the tenants 408 | 409 | #### `AddOrUpdateForAllTenants(string recurringJobId, Job job, string cronExpression, TimeZoneInfo timeZone)` 410 | 411 | It will create a recurring job for all the tenants 412 | 413 | #### `AddOrUpdate(string recurringJobId,Expression> methodCall, string cronExpression, TimeZoneInfo timeZone = null,string queue = "default")` 414 | 415 | It will create a recurring job from the type `T.Method` for the current tenant 416 | 417 | #### `AddOrUpdate(string recurringJobId, Job job, string cronExpression, TimeZoneInfo timeZone)` 418 | 419 | It will create a recurring job for the current tenant 420 | 421 | #### `RemoveIfExistsForAllTenants(string recurringJobId)` 422 | 423 | It will remove the recurring job for all the tenants 424 | 425 | #### `RemoveIfExists(string recurringJobId)` 426 | 427 | It will remove the recurring job for the current tenants 428 | 429 | #### `TriggerForAllTenants(string recurringJobId)` 430 | 431 | It will enqueue the recurring job for all the tenants 432 | 433 | #### `Trigger(string recurringJobId)` 434 | 435 | It will enqueue the recurring job for the current tenants 436 | 437 | ### Background Jobs ⚙️ 438 | 439 | To create Background Jobs you just need to use the interface `IMultiTenantBackgroundJobManager`, this interface will have the following methods and extension methods: 440 | 441 | #### `EnqueueForAllTenants(Expression methodCall)` 442 | 443 | 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 444 | 445 | #### `EnqueueForAllTenants(Expression> methodCall)` 446 | 447 | 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 448 | 449 | #### `EnqueueForAllTenants(Expression> methodCall)` 450 | 451 | 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 452 | 453 | #### `EnqueueForAllTenants(Expression> methodCall)` 454 | 455 | It will enqueue a job execution of type `T.Task` 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 456 | 457 | #### `Enqueue(Expression methodCall)` 458 | 459 | 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 460 | 461 | #### `Enqueue(Expression> methodCall)` 462 | 463 | 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 464 | 465 | #### `Enqueue(Expression> methodCall)` 466 | 467 | 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 468 | 469 | #### `Enqueue(Expression> methodCall)` 470 | 471 | It will enqueue a job execution of type `T.Task` for the current tenant, sending the job for a queue with the tenant's name, the return the jobId 472 | 473 | ### Schedule Jobs ⌚ 474 | 475 | To Schedule Jobs you just need to use the interface `IMultiTenantBackgroundJobManager`, this interface will have the following methods and extension methods: 476 | 477 | #### `ScheduleForAllTenants(Expression methodCall, TimeSpan delay)` 478 | 479 | 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 480 | 481 | #### `ScheduleForAllTenants((Expression> methodCall, TimeSpan delay)` 482 | 483 | 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 484 | 485 | #### `ScheduleForAllTenants(Expression> methodCall, TimeSpan delay)` 486 | 487 | 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 488 | 489 | #### `ScheduleForAllTenants(Expression> methodCall, TimeSpan delay)` 490 | 491 | It will Schedule a job execution of type `T.Task` 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 492 | 493 | #### `Schedule(Expression methodCall, TimeSpan delay)` 494 | 495 | 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 496 | 497 | #### `Schedule(Expression> methodCall, TimeSpan delay)` 498 | 499 | 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 500 | 501 | #### `Schedule(Expression> methodCall, TimeSpan delay)` 502 | 503 | 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 504 | 505 | #### `Schedule(Expression> methodCall, TimeSpan delay)` 506 | 507 | It will enqueue a job execution of type `T.Task` for the current tenant, sending the job for a queue with the tenant's name, returning the Scheduled JobId 508 | 509 | ## Usage / Samples 510 | 511 | You can find some examples of how to use this library in the folder `/samples` with WebApi and Hangfire examples 512 | 513 | ### Web API 514 | 515 | To enable it on your api you first need to add the services: 516 | 517 | ```csharp 518 | builder.Services.AddMultiTenant(); 519 | ``` 520 | 521 | then you need also to register the middleware used to identify the tenant based on the `HttpContext` 522 | 523 | ```csharp 524 | app.UseMultiTenant(); 525 | ``` 526 | 527 | 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: 528 | 529 | Example: 530 | 531 | ```csharp 532 | app.MapGet("/", (IMultiTenantConfiguration configuration) => 533 | { 534 | return new 535 | { 536 | Tenant = configuration.Tenant, 537 | Message = configuration["Message"], 538 | }; 539 | }); 540 | ``` 541 | 542 | ### EF Core Migrations: 543 | 544 | To use it with EF Core is quite simple, you need to use the interface `IMultiTenantConfiguration` as mentioned above to setup your EF Context 545 | 546 | #### Setup 547 | 548 | ```csharp 549 | builder.Services.AddDbContext( 550 | (IServiceProvider serviceProvider, DbContextOptionsBuilder options) => 551 | { 552 | var configuration = serviceProvider.GetRequiredService(); 553 | 554 | options.UseSqlServer(configuration?["ConnectionStrings:DefaultConnection"]); 555 | }); 556 | ``` 557 | 558 | #### Migrations 559 | 560 | To apply the migrations, you only need to do this: 561 | 562 | ```csharp 563 | var tenants = app.Services.GetRequiredService(); 564 | 565 | foreach (var tenant in tenants.Tenants) 566 | { 567 | using (var scope = app.Services.CreateScope()) 568 | { 569 | var tenantSettings = scope.ServiceProvider.GetRequiredService(); 570 | 571 | tenantSettings.SetCurrentTenant(tenant); 572 | 573 | var db = scope.ServiceProvider.GetRequiredService(); 574 | 575 | await db.Database.MigrateAsync(); 576 | } 577 | } 578 | 579 | app.Run(); 580 | ``` 581 | 582 | ## Extras 583 | 584 | 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: 585 | 586 | - `/refresh-settings` 587 | - `/refresh-settings/{tenantName}` 588 | 589 | PS: this will work only with AzureAppConfiguration and SpringCloudConfig 590 | 591 | ## Roadmap 592 | 593 | - [ ] Add unit tests 🧪 594 | - [x] Add new Config Source 595 | - [x] Load the Tenants from a web-api request 596 | - [x] Enable the usage with HangFire 597 | - [x] Update documentation with new features 598 | 599 | See the [open issues](https://github.com/Pub-Dev/Mellon.MultiTenant/issues) for a full list of proposed features (and known issues). 600 | 601 | ## Contributing 602 | 603 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 604 | 605 | 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". 606 | Don't forget to give the project a star! Thanks again! 607 | 608 | 1. Fork the Project 609 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 610 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 611 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 612 | 5. Open a Pull Request 613 | 614 | ## Contact 615 | 616 | - Humberto Rodrigues - [@1bberto](https://instagram.com/1bberto) - humberto_henrique1@live.com 617 | - Rafael Nagai - [@naganaga](https://instagram.com/rafakenji23) - rafakenji23@gmail.com 618 | 619 | Project Page: [https://pub-dev.github.io/Mellon.MultiTenant](https://pub-dev.github.io/Mellon.MultiTenant) 620 | 621 | [contributors-shield]: https://img.shields.io/github/contributors/1bberto/Mellon.MultiTenant.svg?style=for-the-badge 622 | [contributors-url]: https://github.com/Pub-Dev/Mellon.MultiTenant/graphs/contributors 623 | [forks-shield]: https://img.shields.io/github/forks/1bberto/Mellon.MultiTenant.svg?style=for-the-badge 624 | [forks-url]: https://github.com/Pub-Dev/Mellon.MultiTenant/network/members 625 | [stars-shield]: https://img.shields.io/github/stars/1bberto/Mellon.MultiTenant.svg?style=for-the-badge 626 | [stars-url]: https://github.com/Pub-Dev/Mellon.MultiTenant/stargazers 627 | [issues-shield]: https://img.shields.io/github/issues/1bberto/Mellon.MultiTenant.svg?style=for-the-badge 628 | [issues-url]: https://github.com/Pub-Dev/Mellon.MultiTenant/issues 629 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 630 | [linkedin-url]: https://linkedin.com/in/humbberto 631 | [linkedin2-url]: https://br.linkedin.com/in/rafakenji 632 | --------------------------------------------------------------------------------