Skip to content

Support setting ApiParameterRouetInfo for endpoints #37470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument()
var unnamedParameter = Expression.Parameter(typeof(int));
var lambda = Expression.Lambda(Expression.Block(), unnamedParameter);
var ex = Assert.Throws<InvalidOperationException>(() => 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]
Expand Down
1 change: 1 addition & 0 deletions src/Http/Routing/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
{
Expand All @@ -170,7 +169,8 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
DefaultValue = parameter.DefaultValue,
Type = parameter.ParameterType,
IsRequired = !isOptional,
ParameterDescriptor = parameterDescriptor
ParameterDescriptor = parameterDescriptor,
RouteInfo = routeInfo
};
}

Expand All @@ -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<IRouteConstraint>();

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<ActionDescriptor>());

var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().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<ActionDescriptor>());

var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().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<MinLengthRouteConstraint>(constraint),
constraint => Assert.IsType<GuidRouteConstraint>(constraint),
constraint => Assert.IsType<MaxLengthRouteConstraint>(constraint));
}

private static IEnumerable<string> GetSortedMediaTypes(ApiResponseType apiResponseType)
{
return apiResponseType.ApiResponseFormats
Expand All @@ -884,19 +952,21 @@ private static IList<ApiDescription> 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);

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()));

Expand Down