From 65cca984d14d4101616a8b472c0badd63a5c78ef Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 17 Nov 2025 23:14:34 +0000
Subject: [PATCH 1/4] Initial plan
From 373927fd4b188e5831fcc53df739d9600336aade Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 17 Nov 2025 23:31:31 +0000
Subject: [PATCH 2/4] Add RoutePattern field to ApiDescription and update
OpenApi extension to use it
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
---
.../src/ApiExplorer/ApiDescription.cs | 6 +++++
...crosoft.AspNetCore.Mvc.Abstractions.csproj | 1 +
.../src/PublicAPI.Unshipped.txt | 2 ++
.../src/DefaultApiDescriptionProvider.cs | 5 +++++
.../EndpointMetadataApiDescriptionProvider.cs | 1 +
.../src/PublicAPI.Unshipped.txt | 2 ++
.../Extensions/ApiDescriptionExtensions.cs | 2 +-
.../ApiDescriptionExtensionsTests.cs | 22 +++++++++++++++++++
8 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiDescription.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiDescription.cs
index 0f6fe9f6557d..df32c776dfa9 100644
--- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiDescription.cs
+++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiDescription.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -42,6 +43,11 @@ public class ApiDescription
///
public string? RelativePath { get; set; }
+ ///
+ /// Gets or sets the route pattern for this api.
+ ///
+ public RoutePattern? RoutePattern { get; set; }
+
///
/// Gets the list of possible formats for a request.
///
diff --git a/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj b/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
index 16a5411f1caf..0b0c56d86aeb 100644
--- a/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
+++ b/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
@@ -28,6 +28,7 @@ Microsoft.AspNetCore.Mvc.IActionResult
+
diff --git a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..012d03ae8f56 100644
--- a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription.RoutePattern.get -> Microsoft.AspNetCore.Routing.Patterns.RoutePattern?
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription.RoutePattern.set -> void
diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs
index 75f0f194193b..99b8218b35da 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs
+++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs
@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Options;
@@ -97,6 +98,9 @@ private ApiDescription CreateApiDescription(
string? groupName)
{
var parsedTemplate = ParseTemplate(action);
+ var routePattern = parsedTemplate != null && action.AttributeRouteInfo?.Template != null
+ ? RoutePatternFactory.Parse(action.AttributeRouteInfo.Template)
+ : null;
var apiDescription = new ApiDescription()
{
@@ -104,6 +108,7 @@ private ApiDescription CreateApiDescription(
GroupName = groupName,
HttpMethod = httpMethod,
RelativePath = GetRelativePath(parsedTemplate),
+ RoutePattern = routePattern,
};
var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List();
diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
index cdd33a92fe22..0b40e8813269 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
+++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
@@ -102,6 +102,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
HttpMethod = httpMethod,
GroupName = routeEndpoint.Metadata.GetMetadata()?.EndpointGroupName,
RelativePath = routeEndpoint.RoutePattern.RawText?.TrimStart('/'),
+ RoutePattern = routeEndpoint.RoutePattern,
ActionDescriptor = new ActionDescriptor
{
DisplayName = routeEndpoint.DisplayName,
diff --git a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..3cec56717bbb 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
+++ b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription.RoutePattern.get -> Microsoft.AspNetCore.Routing.Patterns.RoutePattern? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescription.RoutePattern.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
index a6e5b86944a1..3fdf20c32fbe 100644
--- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
+++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
@@ -49,7 +49,7 @@ public static string MapRelativePathToItemPath(this ApiDescription apiDescriptio
return "/";
}
var strippedRoute = new StringBuilder();
- var routePattern = RoutePatternFactory.Parse(apiDescription.RelativePath);
+ var routePattern = apiDescription.RoutePattern ?? RoutePatternFactory.Parse(apiDescription.RelativePath);
for (var i = 0; i < routePattern.PathSegments.Count; i++)
{
strippedRoute.Append('/');
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs
index 0fbbce9ac27a..dab07110ffff 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs
@@ -3,6 +3,7 @@
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Routing.Patterns;
public class ApiDescriptionExtensionsTests
{
@@ -31,6 +32,27 @@ public void MapRelativePathToItemPath_ReturnsItemPathForApiDescription(string re
Assert.Equal(expectedItemPath, itemPath);
}
+ [Theory]
+ [InlineData("/~health", "~health", "/~health")]
+ [InlineData("/~api/todos", "~api/todos", "/~api/todos")]
+ [InlineData("/~api/todos/{id}", "~api/todos/{id}", "/~api/todos/{id}")]
+ public void MapRelativePathToItemPath_WithRoutePattern_HandlesRoutesThatStartWithTilde(string rawPattern, string relativePath, string expectedItemPath)
+ {
+ // Arrange
+ var routePattern = RoutePatternFactory.Parse(rawPattern);
+ var apiDescription = new ApiDescription
+ {
+ RelativePath = relativePath,
+ RoutePattern = routePattern
+ };
+
+ // Act
+ var itemPath = apiDescription.MapRelativePathToItemPath();
+
+ // Assert
+ Assert.Equal(expectedItemPath, itemPath);
+ }
+
public static class HttpMethodTestData
{
public static IEnumerable