Skip to content

Commit f8198d8

Browse files
Provide work around for ASP.NET Core bug 41773. Resolves #830
1 parent 551b627 commit f8198d8

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Asp.Versioning.ApiExplorer;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
99
using Microsoft.AspNetCore.Routing;
1010
using Microsoft.AspNetCore.Routing.Patterns;
11+
using Microsoft.AspNetCore.Routing.Template;
1112
using static Asp.Versioning.ApiVersionParameterLocation;
1213
using static System.Linq.Enumerable;
1314
using 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()

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ namespace Microsoft.Extensions.DependencyInjection;
55
using Asp.Versioning;
66
using Asp.Versioning.ApiExplorer;
77
using Microsoft.AspNetCore.Mvc.ApiExplorer;
8+
using Microsoft.AspNetCore.Mvc.ModelBinding;
9+
using Microsoft.AspNetCore.Routing;
810
using Microsoft.Extensions.DependencyInjection.Extensions;
911
using Microsoft.Extensions.Options;
1012
using static ServiceDescriptor;
@@ -60,6 +62,15 @@ private static void AddApiExplorerServices( IServiceCollection services )
6062
services.AddMvcCore().AddApiExplorer();
6163
services.TryAddSingleton<IOptionsFactory<ApiExplorerOptions>, ApiExplorerOptionsFactory<ApiExplorerOptions>>();
6264
services.TryAddSingleton<IApiVersionDescriptionProvider, DefaultApiVersionDescriptionProvider>();
63-
services.TryAddEnumerable( Transient<IApiDescriptionProvider, VersionedApiDescriptionProvider>() );
65+
66+
// use internal constructor until ASP.NET Core fixes their bug
67+
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
68+
services.TryAddEnumerable(
69+
Transient<IApiDescriptionProvider, VersionedApiDescriptionProvider>(
70+
sp => new(
71+
sp.GetRequiredService<ISunsetPolicyManager>(),
72+
sp.GetRequiredService<IModelMetadataProvider>(),
73+
sp.GetRequiredService<IInlineConstraintResolver>(),
74+
sp.GetRequiredService<IOptions<ApiExplorerOptions>>() ) ) );
6475
}
6576
}

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace Asp.Versioning.ApiExplorer;
44

5+
using Asp.Versioning.Routing;
56
using Microsoft.AspNetCore.Mvc.Abstractions;
67
using Microsoft.AspNetCore.Mvc.ApiExplorer;
78
using Microsoft.AspNetCore.Mvc.Controllers;
89
using Microsoft.AspNetCore.Mvc.ModelBinding;
10+
using Microsoft.AspNetCore.Routing;
911
using Microsoft.Extensions.Options;
1012
using static Asp.Versioning.ApiVersionMapping;
1113
using static System.Globalization.CultureInfo;
@@ -18,6 +20,7 @@ namespace Asp.Versioning.ApiExplorer;
1820
public class VersionedApiDescriptionProvider : IApiDescriptionProvider
1921
{
2022
private readonly IOptions<ApiExplorerOptions> options;
23+
private readonly IInlineConstraintResolver constraintResolver;
2124
private ApiVersionModelMetadata? modelMetadata;
2225

2326
/// <summary>
@@ -31,9 +34,19 @@ public VersionedApiDescriptionProvider(
3134
ISunsetPolicyManager sunsetPolicyManager,
3235
IModelMetadataProvider modelMetadataProvider,
3336
IOptions<ApiExplorerOptions> options )
37+
: this( sunsetPolicyManager, modelMetadataProvider, new SimpleConstraintResolver( options ), options ) { }
38+
39+
// intentionally hiding IInlineConstraintResolver from public signature until ASP.NET Core fixes their bug
40+
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
41+
internal VersionedApiDescriptionProvider(
42+
ISunsetPolicyManager sunsetPolicyManager,
43+
IModelMetadataProvider modelMetadataProvider,
44+
IInlineConstraintResolver constraintResolver,
45+
IOptions<ApiExplorerOptions> options )
3446
{
3547
SunsetPolicyManager = sunsetPolicyManager;
3648
ModelMetadataProvider = modelMetadataProvider;
49+
this.constraintResolver = constraintResolver;
3750
this.options = options;
3851
}
3952

@@ -83,6 +96,7 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
8396
var parameterSource = Options.ApiVersionParameterSource;
8497
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, ModelMetadata, Options );
8598

99+
context.ConstraintResolver = constraintResolver;
86100
parameterSource.AddParameters( context );
87101
}
88102

@@ -247,4 +261,21 @@ private IEnumerable<ApiVersion> FlattenApiVersions( IList<ApiDescription> descri
247261

248262
return versions;
249263
}
264+
265+
private sealed class SimpleConstraintResolver : IInlineConstraintResolver
266+
{
267+
private readonly IOptions<ApiExplorerOptions> options;
268+
269+
internal SimpleConstraintResolver( IOptions<ApiExplorerOptions> options ) => this.options = options;
270+
271+
public IRouteConstraint? ResolveConstraint( string inlineConstraint )
272+
{
273+
if ( options.Value.RouteConstraintName == inlineConstraint )
274+
{
275+
return new ApiVersionRouteConstraint();
276+
}
277+
278+
return default;
279+
}
280+
}
250281
}

0 commit comments

Comments
 (0)