diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index f9583b735f69..227be2f5769a 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -208,7 +208,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext { if (parameter.Name is null) { - throw new InvalidOperationException("A parameter does not have a name! Was it generated? All parameters must be named."); + throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name."); } var parameterCustomAttributes = parameter.GetCustomAttributes(); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 3474a2b4794e..c3c9ec733ebf 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -904,7 +904,7 @@ public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() var unnamedParameter = Expression.Parameter(typeof(int)); var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); var ex = Assert.Throws(() => RequestDelegateFactory.Create(lambda.Compile())); - Assert.Equal("A parameter does not have a name! Was it generated? All parameters must be named.", ex.Message); + Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message); } [Fact] diff --git a/src/Http/Routing/src/Properties/AssemblyInfo.cs b/src/Http/Routing/src/Properties/AssemblyInfo.cs index 4d9f8603345f..93c67c32e9c9 100644 --- a/src/Http/Routing/src/Properties/AssemblyInfo.cs +++ b/src/Http/Routing/src/Properties/AssemblyInfo.cs @@ -5,4 +5,5 @@ [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Microbenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ApiExplorer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 29413558b121..3f7be70aec9f 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -25,23 +25,21 @@ internal class EndpointMetadataApiDescriptionProvider : IApiDescriptionProvider private readonly IHostEnvironment _environment; private readonly IServiceProviderIsService? _serviceProviderIsService; private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); + private readonly ParameterPolicyFactory _parameterPolicyFactory; // Executes before MVC's DefaultApiDescriptionProvider and GrpcHttpApiDescriptionProvider for no particular reason. public int Order => -1100; - public EndpointMetadataApiDescriptionProvider(EndpointDataSource endpointDataSource, IHostEnvironment environment) - : this(endpointDataSource, environment, null) - { - } - public EndpointMetadataApiDescriptionProvider( EndpointDataSource endpointDataSource, IHostEnvironment environment, + ParameterPolicyFactory parameterPolicyFactory, IServiceProviderIsService? serviceProviderIsService) { _endpointDataSource = endpointDataSource; _environment = environment; _serviceProviderIsService = serviceProviderIsService; + _parameterPolicyFactory = parameterPolicyFactory; } public void OnProvidersExecuting(ApiDescriptionProviderContext context) @@ -161,6 +159,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string var nullability = nullabilityContext.Create(parameter); var isOptional = parameter.HasDefaultValue || nullability.ReadState != NullabilityState.NotNull || allowEmpty; var parameterDescriptor = CreateParameterDescriptor(parameter); + var routeInfo = CreateParameterRouteInfo(pattern, parameter, isOptional); return new ApiParameterDescription { @@ -170,7 +169,8 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string DefaultValue = parameter.DefaultValue, Type = parameter.ParameterType, IsRequired = !isOptional, - ParameterDescriptor = parameterDescriptor + ParameterDescriptor = parameterDescriptor, + RouteInfo = routeInfo }; } @@ -182,6 +182,41 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param ParameterType = parameter.ParameterType, }; + private ApiParameterRouteInfo? CreateParameterRouteInfo(RoutePattern pattern, ParameterInfo parameter, bool isOptional) + { + if (parameter.Name is null) + { + throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name."); + } + + // Only produce a `RouteInfo` property for parameters that are defined in the route template + if (pattern.GetParameter(parameter.Name) is not RoutePatternParameterPart parameterPart) + { + return null; + } + + var constraints = new List(); + + if (pattern.ParameterPolicies.TryGetValue(parameter.Name, out var parameterPolicyReferences)) + { + foreach (var parameterPolicyReference in parameterPolicyReferences) + { + var policy = _parameterPolicyFactory.Create(parameterPart, parameterPolicyReference); + if (policy is IRouteConstraint generatedConstraint) + { + constraints.Add(generatedConstraint); + } + } + } + + return new ApiParameterRouteInfo() + { + Constraints = constraints.AsReadOnly(), + DefaultValue = parameter.DefaultValue, + IsOptional = isOptional + }; + } + // TODO: Share more of this logic with RequestDelegateFactory.CreateArgument(...) using RequestDelegateFactoryUtilities // which is shared source. private (BindingSource, string, bool, Type) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern) diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index c5686c5a24a8..28f6677eb973 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -502,7 +503,7 @@ public void RespectsProducesProblemExtensionMethod() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -527,7 +528,7 @@ public void RespectsProducesWithGroupNameExtensionMethod() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -552,7 +553,7 @@ public void RespectsExcludeFromDescription() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -578,7 +579,7 @@ public void HandlesProducesWithProducesProblem() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -629,7 +630,7 @@ public void HandleMultipleProduces() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -667,7 +668,7 @@ public void HandleAcceptsMetadata() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -700,7 +701,7 @@ public void HandleAcceptsMetadataWithTypeParameter() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -728,7 +729,7 @@ public void FavorsProducesMetadataOverAttribute() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -763,7 +764,7 @@ public void HandleDefaultIAcceptsMetadataForRequiredBodyParameter() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -801,7 +802,7 @@ public void HandleDefaultIAcceptsMetadataForOptionalBodyParameter() { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -839,7 +840,7 @@ public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBo { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); // Act provider.OnProvidersExecuting(context); @@ -860,6 +861,73 @@ public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBo #nullable restore + [Fact] + public void ProducesRouteInfoOnlyForRouteParameters() + { + var builder = CreateBuilder(); + string GetName(int fromQuery, string name = "default") => $"Hello {name}!"; + builder.MapGet("/api/todos/{name}", GetName); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider( + endpointDataSource, + hostEnvironment, + new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()), + new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var apiDescription = Assert.Single(context.Results); + Assert.Collection(apiDescription.ParameterDescriptions, + parameter => + { + Assert.Equal("fromQuery", parameter.Name); + Assert.Null(parameter.RouteInfo); + }, + parameter => + { + Assert.Equal("name", parameter.Name); + Assert.NotNull(parameter.RouteInfo); + Assert.Empty(parameter.RouteInfo!.Constraints); + Assert.True(parameter.RouteInfo!.IsOptional); + Assert.Equal("default", parameter.RouteInfo!.DefaultValue); + }); + } + + [Fact] + public void HandlesEndpointWithRouteConstraints() + { + var builder = CreateBuilder(); + builder.MapGet("/api/todos/{name:minlength(8):guid:maxlength(20)}", (string name) => ""); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var apiDescription = Assert.Single(context.Results); + var parameter = Assert.Single(apiDescription.ParameterDescriptions); + Assert.NotNull(parameter.RouteInfo); + Assert.Collection(parameter.RouteInfo!.Constraints, + constraint => Assert.IsType(constraint), + constraint => Assert.IsType(constraint), + constraint => Assert.IsType(constraint)); + } + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) { return apiResponseType.ApiResponseFormats @@ -884,12 +952,8 @@ private static IList GetApiDescriptions( var endpoint = new RouteEndpoint(httpContext => Task.CompletedTask, routePattern, 0, endpointMetadata, displayName); var endpointDataSource = new DefaultEndpointDataSource(endpoint); - var hostEnvironment = new HostEnvironment - { - ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) - }; - var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); provider.OnProvidersExecuting(context); provider.OnProvidersExecuted(context); @@ -897,6 +961,12 @@ private static IList GetApiDescriptions( return context.Results; } + private static EndpointMetadataApiDescriptionProvider CreateEndpointMetadataApiDescriptionProvider(EndpointDataSource endpointDataSource) => new EndpointMetadataApiDescriptionProvider( + endpointDataSource, + new HostEnvironment { ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) }, + new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()), + new ServiceProviderIsService()); + private static TestEndpointRouteBuilder CreateBuilder() => new TestEndpointRouteBuilder(new ApplicationBuilder(new TestServiceProvider()));