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