From b595965bf9253fbe79425fa6992a038936c54617 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Tue, 24 Jun 2025 00:40:44 +0300 Subject: [PATCH 1/7] Adds a new tenant resolve contributor --- .../ConfigurationTenantResolveContributor.cs | 27 +++++++++++++++++++ ...igurationTenantResolveOptionsExtensions.cs | 17 ++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs create mode 100644 framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs new file mode 100644 index 0000000000..416fc53d1d --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading.Tasks; +using Volo.Abp; + +namespace Volo.Abp.MultiTenancy; + +public class ConfigurationTenantResolveContributor : TenantResolveContributorBase +{ + public const string ContributorName = "Configuration"; + public override string Name => ContributorName; + + public override async Task ResolveAsync(ITenantResolveContext context) + { + var configuration = context.ServiceProvider.GetRequiredService(); + var tenantIdOrName = configuration["MultiTenancy:Tenant"]; + + if (!tenantIdOrName.IsNullOrEmpty()) + { + context.TenantIdOrName = tenantIdOrName; + } + + await Task.CompletedTask; + } +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs new file mode 100644 index 0000000000..695e16abb7 --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.MultiTenancy; + +public static class ConfigurationTenantResolveOptionsExtensions +{ + public static void AddConfigurationTenantResolver( + this AbpTenantResolveOptions options) + { + options.TenantResolvers.InsertAfter( + r => r is CurrentUserTenantResolveContributor, + new ConfigurationTenantResolveContributor() + ); + } +} From ed3f582ba8c1f8b1236fe2ccfb425c348b643227 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Tue, 24 Jun 2025 00:41:17 +0300 Subject: [PATCH 2/7] add configuration tenant resolver unit test --- ...igurationTenantResolveContributor_Tests.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs diff --git a/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs new file mode 100644 index 0000000000..b5bba89fff --- /dev/null +++ b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; +using Volo.Abp.MultiTenancy; +using Xunit; +using Shouldly; + +public class ConfigurationTenantResolveContributor_Tests : MultiTenancyTestBase +{ + [Fact] + public async Task Should_Resolve_Tenant_From_Configuration() + { + var context = CreateContext("acme"); + + var contributor = new ConfigurationTenantResolveContributor(); + + await contributor.ResolveAsync(context); + + context.TenantIdOrName.ShouldBe("acme"); + } + + [Fact] + public async Task Should_Not_Resolve_If_Configuration_Is_Missing() + { + var context = CreateContext(null); // No tenant set + + var contributor = new ConfigurationTenantResolveContributor(); + + await contributor.ResolveAsync(context); + + context.TenantIdOrName.ShouldBeNull(); + } + + // Reusable setup + private static ITenantResolveContext CreateContext(string? tenantName) + { + var services = new ServiceCollection(); + + services.AddSingleton(new FakeHostEnvironment()); + var configBuilder = new ConfigurationBuilder(); + + if (!string.IsNullOrWhiteSpace(tenantName)) + { + configBuilder.AddInMemoryCollection( + [ + new KeyValuePair("MultiTenancy:Tenant", tenantName) + ]); + } + + services.AddSingleton(configBuilder.Build()); + + var provider = services.BuildServiceProvider(); + return new FakeTenantResolveContext(provider); + } + + // Fake context + private class FakeTenantResolveContext : ITenantResolveContext + { + public IServiceProvider ServiceProvider { get; } + + public string? TenantIdOrName { get; set; } + + public bool Handled { get; set; } + + public FakeTenantResolveContext(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public bool HasResolvedTenantOrHost() + { + return Handled || !string.IsNullOrWhiteSpace(TenantIdOrName); + } + } + + private class FakeHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } = Environments.Development; + public string ApplicationName { get; set; } = "TestApp"; + public string ContentRootPath { get; set; } = ""; + public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); + } + + private class NullFileProvider : IFileProvider + { + public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents(); + public IFileInfo GetFileInfo(string subpath) => new NotFoundFileInfo(subpath); + public Microsoft.Extensions.Primitives.IChangeToken Watch(string filter) => NullChangeToken.Singleton; + } +} From 382c0d3595798332d6b4896b20a94d7087365076 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Tue, 24 Jun 2025 00:43:07 +0300 Subject: [PATCH 3/7] add about ConfigurationTenantResolveContributor in multi-tenancy document --- .../architecture/multi-tenancy/index.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 0a035d110d..e216fac221 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -346,6 +346,42 @@ context.Services ``` +### `ConfigurationTenantResolveContributor` + +This tenant resolver reads the tenant identifier from application configuration `appsettings.json`. + +It is intended primarily for **development or testing environments** where setting the tenant manually is helpful, especially in **non-HTTP** contexts (e.g., console apps, background workers). + +##### **Configuration Example:** + +```json +{ + "MultiTenancy": { + "Tenant": "my-tenant-name" + } +} +``` + +The value can be either the **tenant name** or the **tenant ID**. + +###### **How to Use:** + +This resolver is not registered by default. You can add it manually in your module's `ConfigureServices`: + +```csharp +var env = context.Services.GetHostingEnvironment(); +if (env.IsDevelopment()) // Optional but preferred: only register in development +{ + Configure(options => + { + options.AddConfigurationTenantResolver(); + }); +} +``` + +> Recommended to limit this resolver to development to avoid static tenant resolution in production. + + ##### Custom Tenant Resolvers You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: From b7e139e2a749f3a908cd3b3fc2aaa5afe6e59ada Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Tue, 24 Jun 2025 00:47:11 +0300 Subject: [PATCH 4/7] fix doc headings --- docs/en/framework/architecture/multi-tenancy/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index e216fac221..e0d718d625 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -346,13 +346,13 @@ context.Services ``` -### `ConfigurationTenantResolveContributor` +##### ConfigurationTenantResolveContributor This tenant resolver reads the tenant identifier from application configuration `appsettings.json`. It is intended primarily for **development or testing environments** where setting the tenant manually is helpful, especially in **non-HTTP** contexts (e.g., console apps, background workers). -##### **Configuration Example:** +###### **Configuration Example:** ```json { From 4931b7bd934364c2e4cf86c62a3827e816854b3a Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Tue, 24 Jun 2025 10:30:18 +0300 Subject: [PATCH 5/7] remove non-HTTP from doc --- docs/en/framework/architecture/multi-tenancy/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index e0d718d625..59453f9861 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -350,7 +350,7 @@ context.Services This tenant resolver reads the tenant identifier from application configuration `appsettings.json`. -It is intended primarily for **development or testing environments** where setting the tenant manually is helpful, especially in **non-HTTP** contexts (e.g., console apps, background workers). +It is intended primarily for **development or testing environments** where setting the tenant manually is useful (e.g., without headers, route parameters, or query strings). ###### **Configuration Example:** From 58cbd450ed5a8c3972ab9dff63f0d01b469699b7 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Wed, 25 Jun 2025 00:44:19 +0300 Subject: [PATCH 6/7] DefaultTenantResolveContributor instead of ConfigurationTenantResolveContributor --- .../architecture/multi-tenancy/index.md | 36 +++---- .../AbpAspNetCoreMultiTenancyModule.cs | 1 + .../AbpAspNetCoreMultiTenancyOptions.cs | 6 ++ .../DefaultTenantResolveContributor.cs | 24 +++++ .../ConfigurationTenantResolveContributor.cs | 27 ------ ...igurationTenantResolveOptionsExtensions.cs | 17 ---- ...ltiTenancy_Without_DomainResolver_Tests.cs | 20 ++++ ...igurationTenantResolveContributor_Tests.cs | 94 ------------------- 8 files changed, 66 insertions(+), 159 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs delete mode 100644 framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs delete mode 100644 framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs delete mode 100644 framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 59453f9861..84716a8d63 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -206,6 +206,7 @@ The following resolvers are provided and configured by default; - `RouteTenantResolveContributor`: Tries to find current tenant id from route (URL path). The variable name is `__tenant` by default. If you defined a route with this variable, then it can determine the current tenant from the route. - `HeaderTenantResolveContributor`: Tries to find current tenant id from HTTP headers. The header name is `__tenant` by default. - `CookieTenantResolveContributor`: Tries to find current tenant id from cookie values. The cookie name is `__tenant` by default. +- `DefaultTenantResolveContributor`: Resolves a fallback tenant from configuration if none of the above resolvers succeed. This resolver is automatically registered and runs last by default. It should be configured via `AbpAspNetCoreMultiTenancyOptions.DefaultTenant`, as described in the `Default Tenant Resolver` section below. ###### Problems with the NGINX @@ -346,41 +347,34 @@ context.Services ``` -##### ConfigurationTenantResolveContributor +##### Default Tenant Resolver -This tenant resolver reads the tenant identifier from application configuration `appsettings.json`. +In some cases, especially in **development environments**, resolving a tenant based on domain may not be practical (e.g., due to use of `localhost`). In such cases, a default fallback tenant can be configured using `AbpAspNetCoreMultiTenancyOptions.DefaultTenant`. -It is intended primarily for **development or testing environments** where setting the tenant manually is useful (e.g., without headers, route parameters, or query strings). +This fallback is resolved by the `DefaultTenantResolveContributor`, which attempts to set a default tenant if none of the other resolvers succeed. This contributor is automatically added at the **end** of the tenant resolver list. -###### **Configuration Example:** +###### Configuration + +Set the default tenant value via code or `appsettings.json`: ```json { - "MultiTenancy": { - "Tenant": "my-tenant-name" - } + "MultiTenancy": { + "DefaultTenant": "acme" // can be tenant name or ID + } } ``` -The value can be either the **tenant name** or the **tenant ID**. - -###### **How to Use:** - -This resolver is not registered by default. You can add it manually in your module's `ConfigureServices`: +**Startup Configuration:** ```csharp -var env = context.Services.GetHostingEnvironment(); -if (env.IsDevelopment()) // Optional but preferred: only register in development +Configure(options => { - Configure(options => - { - options.AddConfigurationTenantResolver(); - }); -} + options.DefaultTenant = configuration["MultiTenancy:DefaultTenant"]; +}); ``` -> Recommended to limit this resolver to development to avoid static tenant resolution in production. - +> The `DefaultTenantResolveContributor` must be configured via options as shown above. It is included by default and is evaluated only if all other resolvers fail. ##### Custom Tenant Resolvers diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs index f41d096e75..647406efcc 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs @@ -17,6 +17,7 @@ public class AbpAspNetCoreMultiTenancyModule : AbpModule options.TenantResolvers.Add(new RouteTenantResolveContributor()); options.TenantResolvers.Add(new HeaderTenantResolveContributor()); options.TenantResolvers.Add(new CookieTenantResolveContributor()); + options.TenantResolvers.Add(new DefaultTenantResolveContributor()); }); } } diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs index 31c60b1588..caa618c7e9 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs @@ -26,6 +26,12 @@ namespace Volo.Abp.AspNetCore.MultiTenancy; public class AbpAspNetCoreMultiTenancyOptions { + /// + /// Used by to resolve a fallback tenant + /// when no other tenant resolvers return a value. + /// + public string? DefaultTenant { get; set; } + /// /// Default: . /// diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs new file mode 100644 index 0000000000..d064151dd5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.AspNetCore.MultiTenancy; + +public class DefaultTenantResolveContributor : TenantResolveContributorBase +{ + public const string ContributorName = "Default"; + + public override string Name => ContributorName; + + public override Task ResolveAsync(ITenantResolveContext context) + { + var defaultTenant = context.GetAbpAspNetCoreMultiTenancyOptions().DefaultTenant; + if (!string.IsNullOrWhiteSpace(defaultTenant)) + { + context.TenantIdOrName = defaultTenant; + context.Handled = true; + } + + return Task.CompletedTask; + } +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs deleted file mode 100644 index 416fc53d1d..0000000000 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Threading.Tasks; -using Volo.Abp; - -namespace Volo.Abp.MultiTenancy; - -public class ConfigurationTenantResolveContributor : TenantResolveContributorBase -{ - public const string ContributorName = "Configuration"; - public override string Name => ContributorName; - - public override async Task ResolveAsync(ITenantResolveContext context) - { - var configuration = context.ServiceProvider.GetRequiredService(); - var tenantIdOrName = configuration["MultiTenancy:Tenant"]; - - if (!tenantIdOrName.IsNullOrEmpty()) - { - context.TenantIdOrName = tenantIdOrName; - } - - await Task.CompletedTask; - } -} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs deleted file mode 100644 index 695e16abb7..0000000000 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ConfigurationTenantResolveOptionsExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Volo.Abp.MultiTenancy; - -public static class ConfigurationTenantResolveOptionsExtensions -{ - public static void AddConfigurationTenantResolver( - this AbpTenantResolveOptions options) - { - options.TenantResolvers.InsertAfter( - r => r is CurrentUserTenantResolveContributor, - new ConfigurationTenantResolveContributor() - ); - } -} diff --git a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs index 7776b34b13..56f11a6e7e 100644 --- a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -69,4 +70,23 @@ public class AspNetCoreMultiTenancy_Without_DomainResolver_Tests : AspNetCoreMul var result = await GetResponseAsObjectAsync>("http://abp.io"); result["TenantId"].ShouldBe(_testTenantId.ToString()); } + + [Fact] + public async Task Should_Use_DefaultTenant_If_No_Other_Resolvers_Succeed() + { + _options.DefaultTenant = _testTenantName; + + var result = await GetResponseAsObjectAsync>("http://abp.io"); + + result["TenantId"].ShouldBe(_testTenantId.ToString()); + } + + [Fact] + public async Task Should_Return_404_If_DefaultTenant_Is_Invalid() + { + _options.DefaultTenant = "non-existent-tenant"; + + // This method asserts the status code internally using ShouldBe(...) + await GetResponseAsync("http://abp.io", HttpStatusCode.NotFound); + } } diff --git a/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs deleted file mode 100644 index b5bba89fff..0000000000 --- a/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/ConfigurationTenantResolveContributor_Tests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using System.Collections.Generic; -using System.Threading.Tasks; -using System; -using Volo.Abp.MultiTenancy; -using Xunit; -using Shouldly; - -public class ConfigurationTenantResolveContributor_Tests : MultiTenancyTestBase -{ - [Fact] - public async Task Should_Resolve_Tenant_From_Configuration() - { - var context = CreateContext("acme"); - - var contributor = new ConfigurationTenantResolveContributor(); - - await contributor.ResolveAsync(context); - - context.TenantIdOrName.ShouldBe("acme"); - } - - [Fact] - public async Task Should_Not_Resolve_If_Configuration_Is_Missing() - { - var context = CreateContext(null); // No tenant set - - var contributor = new ConfigurationTenantResolveContributor(); - - await contributor.ResolveAsync(context); - - context.TenantIdOrName.ShouldBeNull(); - } - - // Reusable setup - private static ITenantResolveContext CreateContext(string? tenantName) - { - var services = new ServiceCollection(); - - services.AddSingleton(new FakeHostEnvironment()); - var configBuilder = new ConfigurationBuilder(); - - if (!string.IsNullOrWhiteSpace(tenantName)) - { - configBuilder.AddInMemoryCollection( - [ - new KeyValuePair("MultiTenancy:Tenant", tenantName) - ]); - } - - services.AddSingleton(configBuilder.Build()); - - var provider = services.BuildServiceProvider(); - return new FakeTenantResolveContext(provider); - } - - // Fake context - private class FakeTenantResolveContext : ITenantResolveContext - { - public IServiceProvider ServiceProvider { get; } - - public string? TenantIdOrName { get; set; } - - public bool Handled { get; set; } - - public FakeTenantResolveContext(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - - public bool HasResolvedTenantOrHost() - { - return Handled || !string.IsNullOrWhiteSpace(TenantIdOrName); - } - } - - private class FakeHostEnvironment : IHostEnvironment - { - public string EnvironmentName { get; set; } = Environments.Development; - public string ApplicationName { get; set; } = "TestApp"; - public string ContentRootPath { get; set; } = ""; - public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); - } - - private class NullFileProvider : IFileProvider - { - public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents(); - public IFileInfo GetFileInfo(string subpath) => new NotFoundFileInfo(subpath); - public Microsoft.Extensions.Primitives.IChangeToken Watch(string filter) => NullChangeToken.Singleton; - } -} From 857789aba3b5d125b0db04384abe2a85ff210de8 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Thu, 26 Jun 2025 11:03:55 +0300 Subject: [PATCH 7/7] support fallback tenant in AbpTenantResolveOptions --- .../architecture/multi-tenancy/index.md | 47 ++++------ .../AbpAspNetCoreMultiTenancyModule.cs | 1 - .../AbpAspNetCoreMultiTenancyOptions.cs | 6 -- .../DefaultTenantResolveContributor.cs | 24 ----- .../MultiTenancy/AbpTenantResolveOptions.cs | 5 ++ .../Abp/MultiTenancy/TenantResolverNames.cs | 10 +++ .../Volo/Abp/MultiTenancy/TenantResolver.cs | 6 ++ ...ltiTenancy_Without_DomainResolver_Tests.cs | 19 ---- .../FallbackTenantResolveContributor_Tests.cs | 87 +++++++++++++++++++ 9 files changed, 125 insertions(+), 80 deletions(-) delete mode 100644 framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs create mode 100644 framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs create mode 100644 framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 84716a8d63..bdfd3b9a48 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -206,7 +206,6 @@ The following resolvers are provided and configured by default; - `RouteTenantResolveContributor`: Tries to find current tenant id from route (URL path). The variable name is `__tenant` by default. If you defined a route with this variable, then it can determine the current tenant from the route. - `HeaderTenantResolveContributor`: Tries to find current tenant id from HTTP headers. The header name is `__tenant` by default. - `CookieTenantResolveContributor`: Tries to find current tenant id from cookie values. The cookie name is `__tenant` by default. -- `DefaultTenantResolveContributor`: Resolves a fallback tenant from configuration if none of the above resolvers succeed. This resolver is automatically registered and runs last by default. It should be configured via `AbpAspNetCoreMultiTenancyOptions.DefaultTenant`, as described in the `Default Tenant Resolver` section below. ###### Problems with the NGINX @@ -347,35 +346,6 @@ context.Services ``` -##### Default Tenant Resolver - -In some cases, especially in **development environments**, resolving a tenant based on domain may not be practical (e.g., due to use of `localhost`). In such cases, a default fallback tenant can be configured using `AbpAspNetCoreMultiTenancyOptions.DefaultTenant`. - -This fallback is resolved by the `DefaultTenantResolveContributor`, which attempts to set a default tenant if none of the other resolvers succeed. This contributor is automatically added at the **end** of the tenant resolver list. - -###### Configuration - -Set the default tenant value via code or `appsettings.json`: - -```json -{ - "MultiTenancy": { - "DefaultTenant": "acme" // can be tenant name or ID - } -} -``` - -**Startup Configuration:** - -```csharp -Configure(options => -{ - options.DefaultTenant = configuration["MultiTenancy:DefaultTenant"]; -}); -``` - -> The `DefaultTenantResolveContributor` must be configured via options as shown above. It is included by default and is evaluated only if all other resolvers fail. - ##### Custom Tenant Resolvers You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: @@ -410,6 +380,23 @@ namespace MultiTenancyDemo.Web * A tenant resolver should set `context.TenantIdOrName` if it can determine it. If not, just leave it as is to allow the next resolver to determine it. * `context.ServiceProvider` can be used if you need to additional services to resolve from the [dependency injection](../../fundamentals/dependency-injection.md) system. +##### Fallback Tenant + +In some cases, the tenant cannot be resolved using any of the configured tenant resolvers. To handle such situations, ABP allows setting a **fallback tenant**. + +The fallback tenant can be configured using the `FallbackTenant` property in `AbpTenantResolveOptions`: + +```csharp +Configure(options => +{ + options.FallbackTenant = "default-tenant"; +}); +``` + +If no tenant is resolved and the `FallbackTenant` is not null or empty, ABP will automatically use this value as the current tenant. This provides a simple and consistent way to ensure that a tenant context is always available when needed. + +> **Note:** The fallback tenant is only used if all other resolvers fail. It will never override an already resolved tenant. + #### Multi-Tenancy Middleware Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware) that determines the current tenant from the HTTP request and sets the `ICurrentTenant` properties. diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs index 647406efcc..f41d096e75 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs @@ -17,7 +17,6 @@ public class AbpAspNetCoreMultiTenancyModule : AbpModule options.TenantResolvers.Add(new RouteTenantResolveContributor()); options.TenantResolvers.Add(new HeaderTenantResolveContributor()); options.TenantResolvers.Add(new CookieTenantResolveContributor()); - options.TenantResolvers.Add(new DefaultTenantResolveContributor()); }); } } diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs index caa618c7e9..31c60b1588 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs @@ -26,12 +26,6 @@ namespace Volo.Abp.AspNetCore.MultiTenancy; public class AbpAspNetCoreMultiTenancyOptions { - /// - /// Used by to resolve a fallback tenant - /// when no other tenant resolvers return a value. - /// - public string? DefaultTenant { get; set; } - /// /// Default: . /// diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs deleted file mode 100644 index d064151dd5..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Volo.Abp.MultiTenancy; - -namespace Volo.Abp.AspNetCore.MultiTenancy; - -public class DefaultTenantResolveContributor : TenantResolveContributorBase -{ - public const string ContributorName = "Default"; - - public override string Name => ContributorName; - - public override Task ResolveAsync(ITenantResolveContext context) - { - var defaultTenant = context.GetAbpAspNetCoreMultiTenancyOptions().DefaultTenant; - if (!string.IsNullOrWhiteSpace(defaultTenant)) - { - context.TenantIdOrName = defaultTenant; - context.Handled = true; - } - - return Task.CompletedTask; - } -} diff --git a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs index 414983c87a..26640c3dfd 100644 --- a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs +++ b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs @@ -8,6 +8,11 @@ public class AbpTenantResolveOptions [NotNull] public List TenantResolvers { get; } + /// + /// Fallback tenant to use when no other resolver resolves a tenant. + /// + public string? FallbackTenant { get; set; } + public AbpTenantResolveOptions() { TenantResolvers = new List(); diff --git a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs new file mode 100644 index 0000000000..3542d3a948 --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.MultiTenancy; + +public static class TenantResolverNames +{ + public const string FallbackTenant = "FallbackTenant"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs index 3ae9ae67b1..4351ef62bb 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs @@ -39,6 +39,12 @@ public class TenantResolver : ITenantResolver, ITransientDependency } } + if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(_options.FallbackTenant)) + { + result.TenantIdOrName = _options.FallbackTenant; + result.AppliedResolvers.Add(TenantResolverNames.FallbackTenant); + } + return result; } } diff --git a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs index 56f11a6e7e..1f53e36260 100644 --- a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs @@ -70,23 +70,4 @@ public class AspNetCoreMultiTenancy_Without_DomainResolver_Tests : AspNetCoreMul var result = await GetResponseAsObjectAsync>("http://abp.io"); result["TenantId"].ShouldBe(_testTenantId.ToString()); } - - [Fact] - public async Task Should_Use_DefaultTenant_If_No_Other_Resolvers_Succeed() - { - _options.DefaultTenant = _testTenantName; - - var result = await GetResponseAsObjectAsync>("http://abp.io"); - - result["TenantId"].ShouldBe(_testTenantId.ToString()); - } - - [Fact] - public async Task Should_Return_404_If_DefaultTenant_Is_Invalid() - { - _options.DefaultTenant = "non-existent-tenant"; - - // This method asserts the status code internally using ShouldBe(...) - await GetResponseAsync("http://abp.io", HttpStatusCode.NotFound); - } } diff --git a/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs new file mode 100644 index 0000000000..00bf669aaf --- /dev/null +++ b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.MultiTenancy.ConfigurationStore; +using Xunit; + +namespace Volo.Abp.MultiTenancy; + +public class FallbackTenantResolveContributor_Tests : MultiTenancyTestBase +{ + private readonly Guid _testTenantId = Guid.NewGuid(); + private readonly string _testTenantName = "acme"; + private readonly string _testTenantNormalizedName = "ACME"; + + private readonly AbpTenantResolveOptions _options; + private readonly ITenantResolver _tenantResolver; + + public FallbackTenantResolveContributor_Tests() + { + _options = ServiceProvider.GetRequiredService>().Value; + _tenantResolver = ServiceProvider.GetRequiredService(); + } + + protected override void BeforeAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.Tenants = new[] + { + new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName) + }; + }); + + services.Configure(options => + { + options.FallbackTenant = _testTenantName; + }); + } + + [Fact] + public async Task Should_Resolve_To_Fallback_Tenant_If_No_Other_Contributor_Succeeds() + { + var result = await _tenantResolver.ResolveTenantIdOrNameAsync(); + + result.TenantIdOrName.ShouldBe(_testTenantName); + result.AppliedResolvers.ShouldContain(TenantResolverNames.FallbackTenant); + } + + [Fact] + public async Task Should_Not_Override_Resolved_Tenant() + { + // Arrange + var customTenantName = "resolved-tenant"; + _options.TenantResolvers.Insert(0, new TestTenantResolveContributor(customTenantName)); + + // Act + var result = await _tenantResolver.ResolveTenantIdOrNameAsync(); + + // Assert + result.TenantIdOrName.ShouldBe(customTenantName); + result.AppliedResolvers.First().ShouldBe("Test"); + result.AppliedResolvers.ShouldNotContain(TenantResolverNames.FallbackTenant); + } + + public class TestTenantResolveContributor : TenantResolveContributorBase + { + private readonly string _tenant; + + public TestTenantResolveContributor(string tenant) + { + _tenant = tenant; + } + + public override string Name => "Test"; + + public override Task ResolveAsync(ITenantResolveContext context) + { + context.TenantIdOrName = _tenant; + return Task.CompletedTask; + } + } +} \ No newline at end of file