@@ -8,6 +8,7 @@ namespace Asp.Versioning.ApiExplorer;
88using Microsoft . AspNetCore . Mvc . ModelBinding ;
99using Microsoft . AspNetCore . Routing ;
1010using Microsoft . AspNetCore . Routing . Patterns ;
11+ using Microsoft . AspNetCore . Routing . Template ;
1112using static Asp . Versioning . ApiVersionParameterLocation ;
1213using static System . Linq . Enumerable ;
1314using static System . StringComparison ;
@@ -43,6 +44,11 @@ public ApiVersionParameterDescriptionContext(
4344 optional = options . AssumeDefaultVersionWhenUnspecified && apiVersion == options . DefaultApiVersion ;
4445 }
4546
47+ // intentionally an internal property so the public contract doesn't change. this will be removed
48+ // once the ASP.NET Core team fixes the bug
49+ // BUG: https://github.com/dotnet/aspnetcore/issues/41773
50+ internal IInlineConstraintResolver ? ConstraintResolver { get ; set ; }
51+
4652 /// <summary>
4753 /// Gets the associated API description.
4854 /// </summary>
@@ -160,7 +166,8 @@ protected virtual void AddHeader( string name )
160166 protected virtual void UpdateUrlSegment ( )
161167 {
162168 var parameter = FindByRouteConstraintType ( ApiDescription ) ??
163- FindByRouteConstraintName ( ApiDescription , Options . RouteConstraintName ) ;
169+ FindByRouteConstraintName ( ApiDescription , Options . RouteConstraintName ) ??
170+ TryCreateFromRouteTemplate ( ApiDescription , ConstraintResolver ) ;
164171
165172 if ( parameter == null )
166173 {
@@ -309,6 +316,73 @@ routeInfo.Constraints is IEnumerable<IRouteConstraint> constraints &&
309316 return default ;
310317 }
311318
319+ private static ApiParameterDescription ? TryCreateFromRouteTemplate ( ApiDescription description , IInlineConstraintResolver ? constraintResolver )
320+ {
321+ if ( constraintResolver == null )
322+ {
323+ return default ;
324+ }
325+
326+ var relativePath = description . RelativePath ;
327+
328+ if ( string . IsNullOrEmpty ( relativePath ) )
329+ {
330+ return default ;
331+ }
332+
333+ var constraints = new List < IRouteConstraint > ( ) ;
334+ var template = TemplateParser . Parse ( relativePath ) ;
335+ var constraintName = default ( string ) ;
336+
337+ for ( var i = 0 ; i < template . Parameters . Count ; i ++ )
338+ {
339+ var match = false ;
340+ var parameter = template . Parameters [ i ] ;
341+
342+ foreach ( var inlineConstraint in parameter . InlineConstraints )
343+ {
344+ var constraint = constraintResolver . ResolveConstraint ( inlineConstraint . Constraint ) ! ;
345+
346+ constraints . Add ( constraint ) ;
347+
348+ if ( constraint is ApiVersionRouteConstraint )
349+ {
350+ match = true ;
351+ constraintName = inlineConstraint . Constraint ;
352+ }
353+ }
354+
355+ if ( ! match )
356+ {
357+ continue ;
358+ }
359+
360+ constraints . TrimExcess ( ) ;
361+
362+ // ASP.NET Core does not discover route parameters without using Reflection in 6.0. unclear if it will be fixed before 7.0
363+ // BUG: https://github.com/dotnet/aspnetcore/issues/41773
364+ // REF: https://github.com/dotnet/aspnetcore/blob/release/6.0/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs#L323
365+ var result = new ApiParameterDescription ( )
366+ {
367+ Name = parameter . Name ! ,
368+ RouteInfo = new ( )
369+ {
370+ Constraints = constraints ,
371+ DefaultValue = parameter . DefaultValue ,
372+ IsOptional = parameter . IsOptional || parameter . DefaultValue != null ,
373+ } ,
374+ Source = BindingSource . Path ,
375+ } ;
376+ var token = $ "{ parameter . Name } :{ constraintName } ";
377+
378+ description . RelativePath = relativePath . Replace ( token , parameter . Name , Ordinal ) ;
379+ description . ParameterDescriptions . Insert ( 0 , result ) ;
380+ return result ;
381+ }
382+
383+ return default ;
384+ }
385+
312386 private ApiParameterDescription NewApiVersionParameter ( string name , BindingSource source )
313387 {
314388 var parameter = new ApiParameterDescription ( )
0 commit comments