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