diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 0e499dacddd1..75f0f194193b 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -315,9 +315,21 @@ internal static void ProcessIsRequired(ApiParameterContext context, MvcOptions m parameter.IsRequired = true; } - if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null && !parameter.RouteInfo.IsOptional) + if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null) { - parameter.IsRequired = true; + // Locate the corresponding route parameter metadata. + var routeParam = context.RouteParameters + .FirstOrDefault(rp => string.Equals(rp.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + + // If the parameter is defined as a catch-all, mark it as optional. + if (routeParam != null && routeParam.IsCatchAll) + { + parameter.IsRequired = false; + } + else if (!parameter.RouteInfo.IsOptional) + { + parameter.IsRequired = true; + } } } } diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 41e13c229863..9d5425384d87 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -32,6 +32,36 @@ namespace Microsoft.AspNetCore.Mvc.Description; public class DefaultApiDescriptionProviderTest { + [Fact] + public void OnlyCatchAllParameter_IsReportedAsOptional() + { + // Arrange: Create an action descriptor with a multi-parameter route template. + var action = CreateActionDescriptor(); + action.AttributeRouteInfo = new AttributeRouteInfo + { + Template = "/products/{category}/items/{group}/inventory/{**any}" + }; + + // Act: Get the API descriptions using the existing helper. + var descriptions = GetApiDescriptions(action); + + // Assert: Only the 'any' parameter should be optional. + var description = Assert.Single(descriptions); + var categoryParameter = Assert.Single(description.ParameterDescriptions, + p => string.Equals(p.Name, "category", StringComparison.OrdinalIgnoreCase)); + var groupParameter = Assert.Single(description.ParameterDescriptions, + p => string.Equals(p.Name, "group", StringComparison.OrdinalIgnoreCase)); + var anyParameter = Assert.Single(description.ParameterDescriptions, + p => string.Equals(p.Name, "any", StringComparison.OrdinalIgnoreCase)); + + // The non-catch-all parameters should be required. + Assert.True(categoryParameter.IsRequired); + Assert.True(groupParameter.IsRequired); + + // The catch-all parameter should be optional. + Assert.False(anyParameter.IsRequired); + } + [Fact] public void GetApiDescription_IgnoresNonControllerActionDescriptor() {