Browse Source

support fallback tenant in AbpTenantResolveOptions

pull/23162/head
Suhaib Mousa 8 months ago
parent
commit
857789aba3
  1. 47
      docs/en/framework/architecture/multi-tenancy/index.md
  2. 1
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs
  3. 6
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs
  4. 24
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs
  5. 5
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs
  6. 10
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs
  7. 6
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs
  8. 19
      framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Without_DomainResolver_Tests.cs
  9. 87
      framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs

47
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<AbpAspNetCoreMultiTenancyOptions>(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<AbpTenantResolveOptions>(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.

1
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());
});
}
}

6
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs

@ -26,12 +26,6 @@ namespace Volo.Abp.AspNetCore.MultiTenancy;
public class AbpAspNetCoreMultiTenancyOptions
{
/// <summary>
/// Used by <see cref="DefaultTenantResolveContributor"/> to resolve a fallback tenant
/// when no other tenant resolvers return a value.
/// </summary>
public string? DefaultTenant { get; set; }
/// <summary>
/// Default: <see cref="TenantResolverConsts.DefaultTenantKey"/>.
/// </summary>

24
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DefaultTenantResolveContributor.cs

@ -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;
}
}

5
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs

@ -8,6 +8,11 @@ public class AbpTenantResolveOptions
[NotNull]
public List<ITenantResolveContributor> TenantResolvers { get; }
/// <summary>
/// Fallback tenant to use when no other resolver resolves a tenant.
/// </summary>
public string? FallbackTenant { get; set; }
public AbpTenantResolveOptions()
{
TenantResolvers = new List<ITenantResolveContributor>();

10
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";
}

6
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;
}
}

19
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<Dictionary<string, string>>("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<Dictionary<string, string>>("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);
}
}

87
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<IOptions<AbpTenantResolveOptions>>().Value;
_tenantResolver = ServiceProvider.GetRequiredService<ITenantResolver>();
}
protected override void BeforeAddApplication(IServiceCollection services)
{
services.Configure<AbpDefaultTenantStoreOptions>(options =>
{
options.Tenants = new[]
{
new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName)
};
});
services.Configure<AbpTenantResolveOptions>(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;
}
}
}
Loading…
Cancel
Save