dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.15k stars 9.92k forks source link

Introduce `IParameterBindingMetadata` to remove unbounded reflection in EndpointMetadataApiDescriptionProvider #56587

Closed captainsafia closed 1 month ago

captainsafia commented 2 months ago

Background and Motivation

The EndpointMetadataApiDescriptionProvider is a an implementation of IApiDescriptionProvider that is used to generate ApiDescription instances for minimal APIs that can be used by consumers of ApiExplorer (like Microsoft.AspNetCore.OpenApi) when generating descriptions of the API implementation.

EndpointMetadataApiDescriptionProvider currently relies on unbounded reflection in all scenarios (whether using minimal APIs with static code gen via RequestDelegateGenerator or dynamic code gen via RequestDelegateFactory) to resolve the following information:

It resolves this information using the existing APIs exposed in the ParameterBindingCache. The use of unbounded reflection in this class prevents us from making the entire OpenAPI pipeline trim friendly. To resolve this issue, we will introduce a new metadata type (IParameterBindingMetadata) that will be inserted into endpoints by either RDG or RDF when the endpoints are compiled and used.

Proposed API

// Assembly: Microsoft.AspNetCore.Http.Abstractions

namespace Microsoft.AspNetCore.Http.Metadata;

+ public interface IParameterBindingMetadata
+ {
+   string ParameterName { get; }
+   bool IsTryParsable { get; }
+   bool IsBindAsync { get; }
+   IEnumerable<(ParameterInfo, bool)>? AsParameters { get; }
+ }

public interface IProducesResponseTypeMetadata
{
+   bool IsInferredAwaitable { get; set; }
}

Usage Examples

var parameterBindingMetadata = routeEndpoint.Metadata
    .GetOrderedMetadata<IParameterBindingMetadata>()
    .SingleOrDefault(m => m.ParameterName == parameter.Name);
if (parameterBindingMetadata?.HasTryParse == true)
{
    return BindingSource.Query;
}

Alternative Designs

Risks

amcasey commented 1 month ago

[API Review]

captainsafia commented 1 month ago

Making updates to the proposal in response to the feedback that I received and some additional work in the area:

// Assembly: Microsoft.AspNetCore.Http.Abstractions

namespace Microsoft.AspNetCore.Http.Metadata;

+ public interface IParameterBindingMetadata
+ {
+   string Name { get; }
+   bool HasTryParse { get; }
+   bool HasBindAsync { get; }
+   bool IsOptional { get; }
+   ParameterInfo ParameterInfo { get; }
+ }

public interface IProducesResponseTypeMetadata
{
+   bool IsAwaitable { get; }
}

One open question from review was:

Do we need a full ParameterInfo or can we provide a subset of that info?

Although we only look at certain parts of the ParameterInfo (custom attributes to figure out the binding source, parameter name, type, etc.), ApiExplorer still requires that we encapsulate the ParameterInfo into the EndpointParameterDescriptor object so we can't get away with totally removing references to it (ref).

Or, could it be IEnumerable (to which we might need to add IsOptional)?

I opted to not make the metadata contain an IEnumerable mostly to make unwrapping the collection a little bit easier.

var bindingMetadata = endpoint.Metadata.GetOrderedMetadata<IParameterBindingMetadata>();
// vs
var bindingMetadata = endpoint.Metadata.GetOrderedMetadata<SomeContainerType>().BindingMetadata;
amcasey commented 1 month ago

[API Review]

amcasey commented 1 month ago

Seems promising, but let's try to get rid of IProducesResponseTypeMetadata.IsAwaitable.

IParameterBindingMetadata changes approved!