Skip to content

Commit 1b07f2a

Browse files
authored
Support setting ApiParameterRouetInfo for endpoints (#37470)
* Support setting ApiParameterRoutInfo for endpoints * Address feedback from peer review * Make constraints list read-only * Address more feedback from peer review
1 parent c51d5b3 commit 1b07f2a

File tree

5 files changed

+130
-24
lines changed

5 files changed

+130
-24
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
208208
{
209209
if (parameter.Name is null)
210210
{
211-
throw new InvalidOperationException("A parameter does not have a name! Was it generated? All parameters must be named.");
211+
throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name.");
212212
}
213213

214214
var parameterCustomAttributes = parameter.GetCustomAttributes();

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@ public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument()
904904
var unnamedParameter = Expression.Parameter(typeof(int));
905905
var lambda = Expression.Lambda(Expression.Block(), unnamedParameter);
906906
var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(lambda.Compile()));
907-
Assert.Equal("A parameter does not have a name! Was it generated? All parameters must be named.", ex.Message);
907+
Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message);
908908
}
909909

910910
[Fact]

src/Http/Routing/src/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Microbenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
77
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
8+
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ApiExplorer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
89
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,21 @@ internal class EndpointMetadataApiDescriptionProvider : IApiDescriptionProvider
2525
private readonly IHostEnvironment _environment;
2626
private readonly IServiceProviderIsService? _serviceProviderIsService;
2727
private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new();
28+
private readonly ParameterPolicyFactory _parameterPolicyFactory;
2829

2930
// Executes before MVC's DefaultApiDescriptionProvider and GrpcHttpApiDescriptionProvider for no particular reason.
3031
public int Order => -1100;
3132

32-
public EndpointMetadataApiDescriptionProvider(EndpointDataSource endpointDataSource, IHostEnvironment environment)
33-
: this(endpointDataSource, environment, null)
34-
{
35-
}
36-
3733
public EndpointMetadataApiDescriptionProvider(
3834
EndpointDataSource endpointDataSource,
3935
IHostEnvironment environment,
36+
ParameterPolicyFactory parameterPolicyFactory,
4037
IServiceProviderIsService? serviceProviderIsService)
4138
{
4239
_endpointDataSource = endpointDataSource;
4340
_environment = environment;
4441
_serviceProviderIsService = serviceProviderIsService;
42+
_parameterPolicyFactory = parameterPolicyFactory;
4543
}
4644

4745
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
@@ -161,6 +159,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
161159
var nullability = nullabilityContext.Create(parameter);
162160
var isOptional = parameter.HasDefaultValue || nullability.ReadState != NullabilityState.NotNull || allowEmpty;
163161
var parameterDescriptor = CreateParameterDescriptor(parameter);
162+
var routeInfo = CreateParameterRouteInfo(pattern, parameter, isOptional);
164163

165164
return new ApiParameterDescription
166165
{
@@ -170,7 +169,8 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
170169
DefaultValue = parameter.DefaultValue,
171170
Type = parameter.ParameterType,
172171
IsRequired = !isOptional,
173-
ParameterDescriptor = parameterDescriptor
172+
ParameterDescriptor = parameterDescriptor,
173+
RouteInfo = routeInfo
174174
};
175175
}
176176

@@ -182,6 +182,41 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
182182
ParameterType = parameter.ParameterType,
183183
};
184184

185+
private ApiParameterRouteInfo? CreateParameterRouteInfo(RoutePattern pattern, ParameterInfo parameter, bool isOptional)
186+
{
187+
if (parameter.Name is null)
188+
{
189+
throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name.");
190+
}
191+
192+
// Only produce a `RouteInfo` property for parameters that are defined in the route template
193+
if (pattern.GetParameter(parameter.Name) is not RoutePatternParameterPart parameterPart)
194+
{
195+
return null;
196+
}
197+
198+
var constraints = new List<IRouteConstraint>();
199+
200+
if (pattern.ParameterPolicies.TryGetValue(parameter.Name, out var parameterPolicyReferences))
201+
{
202+
foreach (var parameterPolicyReference in parameterPolicyReferences)
203+
{
204+
var policy = _parameterPolicyFactory.Create(parameterPart, parameterPolicyReference);
205+
if (policy is IRouteConstraint generatedConstraint)
206+
{
207+
constraints.Add(generatedConstraint);
208+
}
209+
}
210+
}
211+
212+
return new ApiParameterRouteInfo()
213+
{
214+
Constraints = constraints.AsReadOnly(),
215+
DefaultValue = parameter.DefaultValue,
216+
IsOptional = isOptional
217+
};
218+
}
219+
185220
// TODO: Share more of this logic with RequestDelegateFactory.CreateArgument(...) using RequestDelegateFactoryUtilities
186221
// which is shared source.
187222
private (BindingSource, string, bool, Type) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern)

src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Mvc.Infrastructure;
1111
using Microsoft.AspNetCore.Mvc.ModelBinding;
1212
using Microsoft.AspNetCore.Routing;
13+
using Microsoft.AspNetCore.Routing.Constraints;
1314
using Microsoft.AspNetCore.Routing.Patterns;
1415
using Microsoft.Extensions.DependencyInjection;
1516
using Microsoft.Extensions.FileProviders;
@@ -502,7 +503,7 @@ public void RespectsProducesProblemExtensionMethod()
502503
{
503504
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
504505
};
505-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
506+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
506507

507508
// Act
508509
provider.OnProvidersExecuting(context);
@@ -527,7 +528,7 @@ public void RespectsProducesWithGroupNameExtensionMethod()
527528
{
528529
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
529530
};
530-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
531+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
531532

532533
// Act
533534
provider.OnProvidersExecuting(context);
@@ -552,7 +553,7 @@ public void RespectsExcludeFromDescription()
552553
{
553554
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
554555
};
555-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
556+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
556557

557558
// Act
558559
provider.OnProvidersExecuting(context);
@@ -578,7 +579,7 @@ public void HandlesProducesWithProducesProblem()
578579
{
579580
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
580581
};
581-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
582+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
582583

583584
// Act
584585
provider.OnProvidersExecuting(context);
@@ -629,7 +630,7 @@ public void HandleMultipleProduces()
629630
{
630631
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
631632
};
632-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
633+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
633634

634635
// Act
635636
provider.OnProvidersExecuting(context);
@@ -667,7 +668,7 @@ public void HandleAcceptsMetadata()
667668
{
668669
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
669670
};
670-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
671+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
671672

672673
// Act
673674
provider.OnProvidersExecuting(context);
@@ -700,7 +701,7 @@ public void HandleAcceptsMetadataWithTypeParameter()
700701
{
701702
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
702703
};
703-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
704+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
704705

705706
// Act
706707
provider.OnProvidersExecuting(context);
@@ -728,7 +729,7 @@ public void FavorsProducesMetadataOverAttribute()
728729
{
729730
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
730731
};
731-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
732+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
732733

733734
// Act
734735
provider.OnProvidersExecuting(context);
@@ -763,7 +764,7 @@ public void HandleDefaultIAcceptsMetadataForRequiredBodyParameter()
763764
{
764765
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
765766
};
766-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
767+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
767768

768769
// Act
769770
provider.OnProvidersExecuting(context);
@@ -801,7 +802,7 @@ public void HandleDefaultIAcceptsMetadataForOptionalBodyParameter()
801802
{
802803
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
803804
};
804-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
805+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
805806

806807
// Act
807808
provider.OnProvidersExecuting(context);
@@ -839,7 +840,7 @@ public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBo
839840
{
840841
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
841842
};
842-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
843+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
843844

844845
// Act
845846
provider.OnProvidersExecuting(context);
@@ -860,6 +861,73 @@ public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBo
860861

861862
#nullable restore
862863

864+
[Fact]
865+
public void ProducesRouteInfoOnlyForRouteParameters()
866+
{
867+
var builder = CreateBuilder();
868+
string GetName(int fromQuery, string name = "default") => $"Hello {name}!";
869+
builder.MapGet("/api/todos/{name}", GetName);
870+
var context = new ApiDescriptionProviderContext(Array.Empty<ActionDescriptor>());
871+
872+
var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().Single();
873+
var hostEnvironment = new HostEnvironment
874+
{
875+
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
876+
};
877+
var provider = new EndpointMetadataApiDescriptionProvider(
878+
endpointDataSource,
879+
hostEnvironment,
880+
new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()),
881+
new ServiceProviderIsService());
882+
883+
// Act
884+
provider.OnProvidersExecuting(context);
885+
886+
// Assert
887+
var apiDescription = Assert.Single(context.Results);
888+
Assert.Collection(apiDescription.ParameterDescriptions,
889+
parameter =>
890+
{
891+
Assert.Equal("fromQuery", parameter.Name);
892+
Assert.Null(parameter.RouteInfo);
893+
},
894+
parameter =>
895+
{
896+
Assert.Equal("name", parameter.Name);
897+
Assert.NotNull(parameter.RouteInfo);
898+
Assert.Empty(parameter.RouteInfo!.Constraints);
899+
Assert.True(parameter.RouteInfo!.IsOptional);
900+
Assert.Equal("default", parameter.RouteInfo!.DefaultValue);
901+
});
902+
}
903+
904+
[Fact]
905+
public void HandlesEndpointWithRouteConstraints()
906+
{
907+
var builder = CreateBuilder();
908+
builder.MapGet("/api/todos/{name:minlength(8):guid:maxlength(20)}", (string name) => "");
909+
var context = new ApiDescriptionProviderContext(Array.Empty<ActionDescriptor>());
910+
911+
var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().Single();
912+
var hostEnvironment = new HostEnvironment
913+
{
914+
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
915+
};
916+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
917+
918+
// Act
919+
provider.OnProvidersExecuting(context);
920+
921+
// Assert
922+
var apiDescription = Assert.Single(context.Results);
923+
var parameter = Assert.Single(apiDescription.ParameterDescriptions);
924+
Assert.NotNull(parameter.RouteInfo);
925+
Assert.Collection(parameter.RouteInfo!.Constraints,
926+
constraint => Assert.IsType<MinLengthRouteConstraint>(constraint),
927+
constraint => Assert.IsType<GuidRouteConstraint>(constraint),
928+
constraint => Assert.IsType<MaxLengthRouteConstraint>(constraint));
929+
}
930+
863931
private static IEnumerable<string> GetSortedMediaTypes(ApiResponseType apiResponseType)
864932
{
865933
return apiResponseType.ApiResponseFormats
@@ -884,19 +952,21 @@ private static IList<ApiDescription> GetApiDescriptions(
884952

885953
var endpoint = new RouteEndpoint(httpContext => Task.CompletedTask, routePattern, 0, endpointMetadata, displayName);
886954
var endpointDataSource = new DefaultEndpointDataSource(endpoint);
887-
var hostEnvironment = new HostEnvironment
888-
{
889-
ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
890-
};
891955

892-
var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
956+
var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
893957

894958
provider.OnProvidersExecuting(context);
895959
provider.OnProvidersExecuted(context);
896960

897961
return context.Results;
898962
}
899963

964+
private static EndpointMetadataApiDescriptionProvider CreateEndpointMetadataApiDescriptionProvider(EndpointDataSource endpointDataSource) => new EndpointMetadataApiDescriptionProvider(
965+
endpointDataSource,
966+
new HostEnvironment { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) },
967+
new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()),
968+
new ServiceProviderIsService());
969+
900970
private static TestEndpointRouteBuilder CreateBuilder() =>
901971
new TestEndpointRouteBuilder(new ApplicationBuilder(new TestServiceProvider()));
902972

0 commit comments

Comments
 (0)