From 58cbd450ed5a8c3972ab9dff63f0d01b469699b7 Mon Sep 17 00:00:00 2001 From: Suhaib Mousa Date: Wed, 25 Jun 2025 00:44:19 +0300 Subject: [PATCH] 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; - } -}