Skip to content

Commit e01e85f

Browse files
authored
Fix: Mark catch-all route parameters as optional (#60392) (#60544)
* Fix: Mark catch-all route parameters as optional (#60392) - Added a unit test in DefaultApiDescriptionProviderTest to validate that catch-all route parameters (e.g., "{**catchAllParameter}") are reported as optional. - Modified ProcessIsRequired in DefaultApiDescriptionProvider to detect catch-all parameters (by checking for the "{**" pattern in the route template) and mark them as not required. - This change aligns ApiExplorer metadata with the actual runtime behavior, ensuring more accurate API documentation. - Follows TDD practices by first writing a failing test and then implementing the fix. * Fix: Use route parameter metadata to correctly mark catch-all parameters as optional Previously, ApiExplorer marked all parameters as optional if the route template contained a catch-all marker ("{**"). This update refines the logic by leveraging ApiParameterContext.RouteParameters and the IsCatchAll flag to identify and mark only the catch-all parameter as optional. This change aligns the API metadata with the actual routing semantics and addresses #60392.
1 parent abfbf9b commit e01e85f

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,21 @@ internal static void ProcessIsRequired(ApiParameterContext context, MvcOptions m
315315
parameter.IsRequired = true;
316316
}
317317

318-
if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null && !parameter.RouteInfo.IsOptional)
318+
if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null)
319319
{
320-
parameter.IsRequired = true;
320+
// Locate the corresponding route parameter metadata.
321+
var routeParam = context.RouteParameters
322+
.FirstOrDefault(rp => string.Equals(rp.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
323+
324+
// If the parameter is defined as a catch-all, mark it as optional.
325+
if (routeParam != null && routeParam.IsCatchAll)
326+
{
327+
parameter.IsRequired = false;
328+
}
329+
else if (!parameter.RouteInfo.IsOptional)
330+
{
331+
parameter.IsRequired = true;
332+
}
321333
}
322334
}
323335
}

src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ namespace Microsoft.AspNetCore.Mvc.Description;
3232

3333
public class DefaultApiDescriptionProviderTest
3434
{
35+
[Fact]
36+
public void OnlyCatchAllParameter_IsReportedAsOptional()
37+
{
38+
// Arrange: Create an action descriptor with a multi-parameter route template.
39+
var action = CreateActionDescriptor();
40+
action.AttributeRouteInfo = new AttributeRouteInfo
41+
{
42+
Template = "/products/{category}/items/{group}/inventory/{**any}"
43+
};
44+
45+
// Act: Get the API descriptions using the existing helper.
46+
var descriptions = GetApiDescriptions(action);
47+
48+
// Assert: Only the 'any' parameter should be optional.
49+
var description = Assert.Single(descriptions);
50+
var categoryParameter = Assert.Single(description.ParameterDescriptions,
51+
p => string.Equals(p.Name, "category", StringComparison.OrdinalIgnoreCase));
52+
var groupParameter = Assert.Single(description.ParameterDescriptions,
53+
p => string.Equals(p.Name, "group", StringComparison.OrdinalIgnoreCase));
54+
var anyParameter = Assert.Single(description.ParameterDescriptions,
55+
p => string.Equals(p.Name, "any", StringComparison.OrdinalIgnoreCase));
56+
57+
// The non-catch-all parameters should be required.
58+
Assert.True(categoryParameter.IsRequired);
59+
Assert.True(groupParameter.IsRequired);
60+
61+
// The catch-all parameter should be optional.
62+
Assert.False(anyParameter.IsRequired);
63+
}
64+
3565
[Fact]
3666
public void GetApiDescription_IgnoresNonControllerActionDescriptor()
3767
{

0 commit comments

Comments
 (0)