From 4d4458f64cdef79701d79c95c37fa2f2792f8d6e Mon Sep 17 00:00:00 2001 From: Reza Heidari Date: Tue, 13 Jan 2026 16:14:20 +0300 Subject: [PATCH 1/2] fix: read minimal api route patterns for openapi fallback to RoutePattern when attribute templates are missing so tenant paths retain {tenantId} in ApiExplorer test the RoutePattern fallback to prevent regressions --- .../TenantRouteApiDescriptionProvider.cs | 43 +++++++++++++++++- .../TenantRouteApiDescriptionProviderTests.cs | 44 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/CleanArchitecture.Extensions.Multitenancy.AspNetCore/ApiExplorer/TenantRouteApiDescriptionProvider.cs b/src/CleanArchitecture.Extensions.Multitenancy.AspNetCore/ApiExplorer/TenantRouteApiDescriptionProvider.cs index d9c835e..a70534c 100644 --- a/src/CleanArchitecture.Extensions.Multitenancy.AspNetCore/ApiExplorer/TenantRouteApiDescriptionProvider.cs +++ b/src/CleanArchitecture.Extensions.Multitenancy.AspNetCore/ApiExplorer/TenantRouteApiDescriptionProvider.cs @@ -1,5 +1,7 @@ +using System.Reflection; using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Options; using CleanArchitecture.Extensions.Multitenancy.Configuration; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; @@ -46,7 +48,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) foreach (var description in context.Results) { - var template = description.ActionDescriptor?.AttributeRouteInfo?.Template; + var template = GetTemplate(description.ActionDescriptor); if (!ShouldApplyTemplate(template)) { continue; @@ -89,6 +91,45 @@ private static string NormalizeTemplate(string template) return template.TrimStart('/'); } + private static string? GetTemplate(ActionDescriptor? actionDescriptor) + { + if (actionDescriptor is null) + { + return null; + } + + var template = actionDescriptor.AttributeRouteInfo?.Template; + if (!string.IsNullOrWhiteSpace(template)) + { + return template; + } + + return TryGetRoutePatternTemplate(actionDescriptor); + } + + private static string? TryGetRoutePatternTemplate(ActionDescriptor actionDescriptor) + { + var routePatternProperty = actionDescriptor.GetType().GetProperty("RoutePattern", BindingFlags.Instance | BindingFlags.Public); + if (routePatternProperty is null) + { + return null; + } + + var routePattern = routePatternProperty.GetValue(actionDescriptor); + if (routePattern is null) + { + return null; + } + + var rawTextProperty = routePattern.GetType().GetProperty("RawText", BindingFlags.Instance | BindingFlags.Public); + if (rawTextProperty?.PropertyType == typeof(string)) + { + return rawTextProperty.GetValue(routePattern) as string; + } + + return routePattern.ToString(); + } + private static bool ContainsRouteParameter(string template, string parameterName) { if (string.IsNullOrWhiteSpace(template) || string.IsNullOrWhiteSpace(parameterName)) diff --git a/tests/CleanArchitecture.Extensions.Multitenancy.AspNetCore.Tests/TenantRouteApiDescriptionProviderTests.cs b/tests/CleanArchitecture.Extensions.Multitenancy.AspNetCore.Tests/TenantRouteApiDescriptionProviderTests.cs index 8ebd967..74fff47 100644 --- a/tests/CleanArchitecture.Extensions.Multitenancy.AspNetCore.Tests/TenantRouteApiDescriptionProviderTests.cs +++ b/tests/CleanArchitecture.Extensions.Multitenancy.AspNetCore.Tests/TenantRouteApiDescriptionProviderTests.cs @@ -73,4 +73,48 @@ public void OnProvidersExecuting_noops_when_openapi_integration_disabled() Assert.Equal("api/tenants/TodoItems", description.RelativePath); Assert.Empty(description.ParameterDescriptions); } + + [Fact] + public void OnProvidersExecuting_uses_route_pattern_when_attribute_template_missing() + { + var options = OptionsFactory.Create(new MultitenancyOptions { RouteParameterName = "tenantId" }); + var aspNetCoreOptions = OptionsFactory.Create(new AspNetCoreMultitenancyOptions { EnableOpenApiIntegration = true }); + var provider = new TenantRouteApiDescriptionProvider(options, aspNetCoreOptions, new EmptyModelMetadataProvider()); + + var actionDescriptor = new StubRouteActionDescriptor + { + RoutePattern = new StubRoutePattern("api/tenants/{tenantId}/TodoItems") + }; + + var description = new ApiDescription + { + ActionDescriptor = actionDescriptor, + RelativePath = "api/tenants/TodoItems" + }; + + var context = new ApiDescriptionProviderContext(new[] { actionDescriptor }); + context.Results.Add(description); + + provider.OnProvidersExecuting(context); + + Assert.Equal("api/tenants/{tenantId}/TodoItems", description.RelativePath); + var parameter = Assert.Single(description.ParameterDescriptions, item => + string.Equals(item.Name, "tenantId", StringComparison.OrdinalIgnoreCase)); + Assert.Equal(BindingSource.Path, parameter.Source); + } + + private sealed class StubRoutePattern + { + public StubRoutePattern(string rawText) + { + RawText = rawText; + } + + public string RawText { get; } + } + + private sealed class StubRouteActionDescriptor : ActionDescriptor + { + public StubRoutePattern? RoutePattern { get; set; } + } } From b73f6cf27953e00d61fc098d624356fbbfbfdad2 Mon Sep 17 00:00:00 2001 From: Reza Heidari Date: Tue, 13 Jan 2026 16:14:39 +0300 Subject: [PATCH 2/2] chore(samples): bump multitenancy packages to 0.2.7 update sample project references and docs to reflect the v0.2.7 package versions --- docs/samples/multitenancy/header-and-route-resolution.md | 4 ++-- .../README.md | 6 +++--- .../src/Application/Application.csproj | 4 ++-- .../src/Web/Web.csproj | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/samples/multitenancy/header-and-route-resolution.md b/docs/samples/multitenancy/header-and-route-resolution.md index 972e919..a271956 100644 --- a/docs/samples/multitenancy/header-and-route-resolution.md +++ b/docs/samples/multitenancy/header-and-route-resolution.md @@ -37,13 +37,13 @@ Document a sample that shows deterministic tenant resolution from route first, h - `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj`: ```xml - + ``` - `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj`: ```xml - + ``` 3. Configure `MultitenancyOptions` for route-first ordering (`Route > Host > Header > Query > Claim`), set header name `X-Tenant-ID`, require tenants by default, allow explicitly anonymous endpoints, and disable fallback tenants. diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md index bfa5b0d..9784896 100644 --- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md +++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md @@ -1,4 +1,4 @@ -# CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution +# CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution The project was generated using the [Clean.Architecture.Solution.Template](https://github.com/jasontaylordev/CleanArchitecture) version 10.0.0-preview. @@ -17,14 +17,14 @@ Packages: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj`: ```xml - + ``` `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj`: ```xml - + ``` diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj index 5a95057..0307675 100644 --- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj +++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj @@ -1,4 +1,4 @@ - + CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Application @@ -9,7 +9,7 @@ - + diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj index 8ffa3ac..eb56967 100644 --- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj +++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj @@ -1,4 +1,4 @@ - + CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Web @@ -14,7 +14,7 @@ - +