Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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