diff --git a/src/Http/Http.Abstractions/src/EndpointFilterFactoryContext.cs b/src/Http/Http.Abstractions/src/EndpointFilterFactoryContext.cs index 86ab54683508..72e2d88f1727 100644 --- a/src/Http/Http.Abstractions/src/EndpointFilterFactoryContext.cs +++ b/src/Http/Http.Abstractions/src/EndpointFilterFactoryContext.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; -using Microsoft.AspNetCore.Builder; namespace Microsoft.AspNetCore.Http; @@ -13,34 +12,21 @@ namespace Microsoft.AspNetCore.Http; public sealed class EndpointFilterFactoryContext { /// - /// Creates a new instance of the . + /// The associated with the current route handler, or MVC action. /// - /// The associated with the route handler of the current request. - /// The associated with the endpoint the filter is targeting. - /// The instance used to access the application services. - public EndpointFilterFactoryContext(MethodInfo methodInfo, IList endpointMetadata, IServiceProvider applicationServices) - { - ArgumentNullException.ThrowIfNull(methodInfo); - ArgumentNullException.ThrowIfNull(endpointMetadata); - ArgumentNullException.ThrowIfNull(applicationServices); - - MethodInfo = methodInfo; - EndpointMetadata = endpointMetadata; - ApplicationServices = applicationServices; - } - - /// - /// The associated with the current route handler. - /// - public MethodInfo MethodInfo { get; } - - /// - /// The associated with the current endpoint. - /// - public IList EndpointMetadata { get; } + /// + /// In the future this could support more endpoint types. + /// + public required MethodInfo MethodInfo { get; init; } /// /// Gets the instance used to access application services. /// - public IServiceProvider ApplicationServices { get; } + public IServiceProvider ApplicationServices { get; init; } = EmptyServiceProvider.Instance; + + private sealed class EmptyServiceProvider : IServiceProvider + { + public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider(); + public object? GetService(Type serviceType) => null; + } } diff --git a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs index 3afd75bb4091..c767c4efe954 100644 --- a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs +++ b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs @@ -10,10 +10,12 @@ namespace Microsoft.AspNetCore.Builder; /// public abstract class EndpointBuilder { + private List>? _filterFactories; + /// /// Gets the list of filters that apply to this endpoint. /// - public IList> FilterFactories { get; } = new List>(); + public IList> FilterFactories => _filterFactories ??= new(); /// /// Gets or sets the delegate used to process requests for the endpoint. @@ -33,7 +35,7 @@ public abstract class EndpointBuilder /// /// Gets the associated with the endpoint. /// - public IServiceProvider ApplicationServices { get; set; } = EmptyServiceProvider.Instance; + public IServiceProvider ApplicationServices { get; init; } = EmptyServiceProvider.Instance; /// /// Creates an instance of from the . diff --git a/src/Http/Http.Abstractions/src/Metadata/IEndpointMetadataProvider.cs b/src/Http/Http.Abstractions/src/Metadata/IEndpointMetadataProvider.cs new file mode 100644 index 000000000000..ec7fa024b090 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IEndpointMetadataProvider.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Indicates that a type provides a static method that provides metadata when declared as a parameter type or the +/// returned type of an route handler delegate. +/// +public interface IEndpointMetadataProvider +{ + /// + /// Populates metadata for the related and . + /// + /// + /// This method is called by RequestDelegateFactory when creating a and by MVC when creating endpoints for controller actions. + /// This is called for each parameter and return type of the route handler or action with a declared type implementing this interface. + /// Add or remove objects on the property of the to modify the being built. + /// + /// The of the route handler delegate or MVC Action of the endpoint being created. + /// The used to construct the endpoint for the given . + static abstract void PopulateMetadata(MethodInfo method, EndpointBuilder builder); +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IEndpointParameterMetadataProvider.cs b/src/Http/Http.Abstractions/src/Metadata/IEndpointParameterMetadataProvider.cs new file mode 100644 index 000000000000..2f9a45dac57d --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IEndpointParameterMetadataProvider.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Indicates that a type provides a static method that provides metadata when declared as the +/// parameter type of an route handler delegate. +/// +public interface IEndpointParameterMetadataProvider +{ + /// + /// Populates metadata for the related and . + /// + /// + /// This method is called by RequestDelegateFactory when creating a and by MVC when creating endpoints for controller actions. + /// This is called for each parameter of the route handler or action with a declared type implementing this interface. + /// Add or remove objects on the property of the to modify the being built. + /// + /// The of the route handler delegate or MVC Action of the endpoint being created. + /// The used to construct the endpoint for the given . + static abstract void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder); +} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index c441d489f7e5..121083081390 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -5,7 +5,7 @@ abstract Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.Arguments.get abstract Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.GetArgument(int index) -> T abstract Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! Microsoft.AspNetCore.Builder.EndpointBuilder.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.Builder.EndpointBuilder.ApplicationServices.set -> void +Microsoft.AspNetCore.Builder.EndpointBuilder.ApplicationServices.init -> void Microsoft.AspNetCore.Builder.EndpointBuilder.FilterFactories.get -> System.Collections.Generic.IList!>! Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.Finally(System.Action! finallyConvention) -> void Microsoft.AspNetCore.Http.AsParametersAttribute @@ -16,9 +16,10 @@ Microsoft.AspNetCore.Http.DefaultEndpointFilterInvocationContext.DefaultEndpoint Microsoft.AspNetCore.Http.EndpointFilterDelegate Microsoft.AspNetCore.Http.EndpointFilterFactoryContext Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.EndpointFilterFactoryContext(System.Reflection.MethodInfo! methodInfo, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void -Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.EndpointMetadata.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.ApplicationServices.init -> void +Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.EndpointFilterFactoryContext() -> void Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.MethodInfo.get -> System.Reflection.MethodInfo! +Microsoft.AspNetCore.Http.EndpointFilterFactoryContext.MethodInfo.init -> void Microsoft.AspNetCore.Http.EndpointFilterInvocationContext Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.EndpointFilterInvocationContext() -> void Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object! @@ -68,6 +69,10 @@ abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string? Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string! +Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider +Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata(System.Reflection.MethodInfo! method, Microsoft.AspNetCore.Builder.EndpointBuilder! builder) -> void +Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider +Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider.PopulateMetadata(System.Reflection.ParameterInfo! parameter, Microsoft.AspNetCore.Builder.EndpointBuilder! builder) -> void Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata.Summary.get -> string! Microsoft.AspNetCore.Mvc.ProblemDetails diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs deleted file mode 100644 index caf96455684c..000000000000 --- a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Represents the information accessible during endpoint creation by types that implement . -/// -public sealed class EndpointMetadataContext -{ - /// - /// Creates a new instance of the class. - /// - /// The of the route handler delegate of the endpoint being created. - /// The list of objects that will be added to the metadata of the endpoint. - /// The instance used to access application services. - public EndpointMetadataContext(MethodInfo method, IList endpointMetadata, IServiceProvider applicationServices) - { - ArgumentNullException.ThrowIfNull(method); - ArgumentNullException.ThrowIfNull(endpointMetadata); - ArgumentNullException.ThrowIfNull(applicationServices); - - Method = method; - EndpointMetadata = endpointMetadata; - ApplicationServices = applicationServices; - } - - /// - /// Gets the of the route handler delegate of the endpoint being created. - /// - public MethodInfo Method { get; } - - /// - /// Gets the list of objects that will be added to the metadata of the endpoint. - /// - public IList EndpointMetadata { get; } - - /// - /// Gets the instance used to access application services. - /// - public IServiceProvider ApplicationServices { get; } -} diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs deleted file mode 100644 index 202839cc8080..000000000000 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Represents the information accessible during endpoint creation by types that implement . -/// -public sealed class EndpointParameterMetadataContext -{ - /// - /// Creates a new instance of the class. - /// - /// The parameter of the route handler delegate of the endpoint being created. - /// The list of objects that will be added to the metadata of the endpoint. - /// The instance used to access application services. - public EndpointParameterMetadataContext(ParameterInfo parameter, IList endpointMetadata, IServiceProvider applicationServices) - { - ArgumentNullException.ThrowIfNull(parameter); - ArgumentNullException.ThrowIfNull(endpointMetadata); - ArgumentNullException.ThrowIfNull(applicationServices); - - Parameter = parameter; - EndpointMetadata = endpointMetadata; - ApplicationServices = applicationServices; - } - - /// - /// Gets the parameter of the route handler delegate of the endpoint being created. - /// - public ParameterInfo Parameter { get; } - - /// - /// Gets the list of objects that will be added to the metadata of the endpoint. - /// - public IList EndpointMetadata { get; } - - /// - /// Gets the instance used to access application services. - /// - public IServiceProvider ApplicationServices { get; } -} diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs deleted file mode 100644 index b7bda01e4716..000000000000 --- a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Indicates that a type provides a static method that provides metadata when declared as a parameter type or the -/// returned type of an route handler delegate. -/// -public interface IEndpointMetadataProvider -{ - /// - /// Populates metadata for the related . - /// - /// - /// This method is called by when creating a . - /// The property of will contain - /// the initial metadata for the endpoint.
- /// Add or remove objects on to affect the metadata of the endpoint. - ///
- /// The . - static abstract void PopulateMetadata(EndpointMetadataContext context); -} diff --git a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs deleted file mode 100644 index 45c29dbbf347..000000000000 --- a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Indicates that a type provides a static method that provides metadata when declared as the -/// parameter type of an route handler delegate. -/// -public interface IEndpointParameterMetadataProvider -{ - /// - /// Populates metadata for the related . - /// - /// - /// This method is called by when creating a . - /// The property of will contain - /// the initial metadata for the endpoint.
- /// Add or remove objects on to affect the metadata of the endpoint. - ///
- /// The . - static abstract void PopulateMetadata(EndpointParameterMetadataContext parameterContext); -} diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index ed2fbb631a9b..d0041e24175d 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state. @@ -14,6 +14,7 @@ + diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 4b8eccfa6afb..6945deb350e5 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -7,20 +7,6 @@ Microsoft.AspNetCore.Http.HttpValidationProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.get -> System.Collections.Generic.IDictionary! (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails() -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IDictionary! errors) -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext(System.Reflection.ParameterInfo! parameter, System.Collections.Generic.IList! endpointMetadata, System.IServiceProvider! applicationServices) -> void -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! -Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider -Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext! context) -> void -Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider -Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext! parameterContext) -> void Microsoft.AspNetCore.Http.ProblemDetailsOptions Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.get -> System.Action? Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails.set -> void @@ -38,6 +24,12 @@ Microsoft.AspNetCore.Http.ProblemDetailsOptions.ProblemDetailsOptions() -> void *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? *REMOVED*Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointBuilder.get -> Microsoft.AspNetCore.Builder.EndpointBuilder? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointBuilder.init -> void +Microsoft.AspNetCore.Http.RequestDelegateMetadataResult +Microsoft.AspNetCore.Http.RequestDelegateMetadataResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Http.RequestDelegateMetadataResult.EndpointMetadata.init -> void +Microsoft.AspNetCore.Http.RequestDelegateMetadataResult.RequestDelegateMetadataResult() -> void Microsoft.AspNetCore.Mvc.ProblemDetails (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) @@ -52,15 +44,18 @@ Microsoft.AspNetCore.Mvc.ProblemDetails.Title.set -> void (forwarded, contained Microsoft.AspNetCore.Mvc.ProblemDetails.Type.get -> string? (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.AspNetCore.Mvc.ProblemDetails.Type.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions) Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointFilterFactories.init -> void -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.get -> System.Collections.Generic.IList? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.EndpointMetadata.init -> void Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Type! type, System.Text.Json.Serialization.JsonSerializerContext! context, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, TValue value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +*REMOVED*static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! handler, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +*REMOVED*static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! handler, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult? metadataResult = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! handler, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult? metadataResult = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func? targetFactory, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options) -> Microsoft.AspNetCore.Http.RequestDelegateResult! +static Microsoft.AspNetCore.Http.RequestDelegateFactory.InferMetadata(System.Reflection.MethodInfo! methodInfo, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateMetadataResult! static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.ProblemDetailsServiceCollectionExtensions.AddProblemDetails(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.HttpJsonServiceExtensions.ConfigureHttpJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 810d902964c1..9d141f00f658 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -9,10 +9,10 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security.Claims; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; @@ -55,8 +55,6 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo TaskOfTToValueTaskOfObjectMethod = typeof(RequestDelegateFactory).GetMethod(nameof(TaskOfTToValueTaskOfObject), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ValueTaskOfTToValueTaskOfObjectMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ValueTaskOfTToValueTaskOfObject), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ArrayEmptyOfObjectMethod = typeof(Array).GetMethod(nameof(Array.Empty), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(new Type[] { typeof(object) }); private static readonly PropertyInfo QueryIndexerProperty = typeof(IQueryCollection).GetProperty("Item")!; @@ -115,14 +113,52 @@ public static partial class RequestDelegateFactory private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" }; private static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + /// + /// Returns metadata inferred automatically for the created by . + /// This includes metadata inferred by and implemented by parameter and return types to the . + /// + /// The for the route handler to be passed to . + /// The options that will be used when calling . + /// The to be passed to . + [RequiresUnreferencedCode("RequestDelegateFactory performs object creation, serialization and deserialization on the delegates and its parameters. This cannot be statically analyzed.")] + public static RequestDelegateMetadataResult InferMetadata(MethodInfo methodInfo, RequestDelegateFactoryOptions? options = null) + { + var factoryContext = CreateFactoryContext(options); + factoryContext.ArgumentExpressions = CreateArgumentsAndInferMetadata(methodInfo, factoryContext); + + return new RequestDelegateMetadataResult + { + EndpointMetadata = AsReadOnlyList(factoryContext.EndpointBuilder.Metadata), + }; + } + + /// + /// Creates a implementation for . + /// + /// A request handler with any number of custom parameters that often produces a response with its return value. + /// The used to configure the behavior of the handler. + /// The . + [RequiresUnreferencedCode("RequestDelegateFactory performs object creation, serialization and deserialization on the delegates and its parameters. This cannot be statically analyzed.")] + public static RequestDelegateResult Create(Delegate handler, RequestDelegateFactoryOptions? options) + { + return Create(handler, options, metadataResult: null); + } + /// /// Creates a implementation for . /// /// A request handler with any number of custom parameters that often produces a response with its return value. /// The used to configure the behavior of the handler. + /// + /// The result returned from if that was used to inferring metadata before creating the final RequestDelegate. + /// If , this call to method will infer the metadata that + /// would have inferred for the same and populate + /// with that metadata. Otherwise, this metadata inference will be skipped as this step has already been done. + /// /// The . [RequiresUnreferencedCode("RequestDelegateFactory performs object creation, serialization and deserialization on the delegates and its parameters. This cannot be statically analyzed.")] - public static RequestDelegateResult Create(Delegate handler, RequestDelegateFactoryOptions? options = null) + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] + public static RequestDelegateResult Create(Delegate handler, RequestDelegateFactoryOptions? options = null, RequestDelegateMetadataResult? metadataResult = null) { if (handler is null) { @@ -135,20 +171,20 @@ public static RequestDelegateResult Create(Delegate handler, RequestDelegateFact null => null, }; - var factoryContext = CreateFactoryContext(options, handler); + var factoryContext = CreateFactoryContext(options, metadataResult, handler); Expression> targetFactory = (httpContext) => handler.Target; - var targetableRequestDelegate = CreateTargetableRequestDelegate(handler.Method, targetExpression, factoryContext, targetFactory); - if (targetableRequestDelegate is null) + RequestDelegate finalRequestDelegate = targetableRequestDelegate switch { // handler is a RequestDelegate that has not been modified by a filter. Short-circuit and return the original RequestDelegate back. // It's possible a filter factory has still modified the endpoint metadata though. - return new RequestDelegateResult((RequestDelegate)handler, AsReadOnlyList(factoryContext.Metadata)); - } + null => (RequestDelegate)handler, + _ => httpContext => targetableRequestDelegate(handler.Target, httpContext), + }; - return new RequestDelegateResult(httpContext => targetableRequestDelegate(handler.Target, httpContext), AsReadOnlyList(factoryContext.Metadata)); + return CreateRequestDelegateResult(finalRequestDelegate, factoryContext.EndpointBuilder); } /// @@ -158,9 +194,28 @@ public static RequestDelegateResult Create(Delegate handler, RequestDelegateFact /// Creates the for the non-static method. /// The used to configure the behavior of the handler. /// The . + [RequiresUnreferencedCode("RequestDelegateFactory performs object creation, serialization and deserialization on the delegates and its parameters. This cannot be statically analyzed.")] + public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory, RequestDelegateFactoryOptions? options) + { + return Create(methodInfo, targetFactory, options, metadataResult: null); + } + /// + /// Creates a implementation for . + /// + /// A request handler with any number of custom parameters that often produces a response with its return value. + /// Creates the for the non-static method. + /// The used to configure the behavior of the handler. + /// + /// The result returned from if that was used to inferring metadata before creating the final RequestDelegate. + /// If , this call to method will infer the metadata that + /// would have inferred for the same and populate + /// with that metadata. Otherwise, this metadata inference will be skipped as this step has already been done. + /// + /// The . [RequiresUnreferencedCode("RequestDelegateFactory performs object creation, serialization and deserialization on the delegates and its parameters. This cannot be statically analyzed.")] - public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null, RequestDelegateMetadataResult? metadataResult = null) { if (methodInfo is null) { @@ -172,45 +227,66 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func untargetableRequestDelegate(null, httpContext), AsReadOnlyList(factoryContext.Metadata)); - } - - targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; + finalRequestDelegate = httpContext => untargetableRequestDelegate(null, httpContext); } + else + { + targetFactory ??= context => Activator.CreateInstance(methodInfo.DeclaringType)!; - var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); - var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression, factoryContext, context => targetFactory(context)); + var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); + var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression, factoryContext, context => targetFactory(context)); - // CreateTargetableRequestDelegate can only return null given a RequestDelegate passed into the other RDF.Create() overload. - Debug.Assert(targetableRequestDelegate is not null); + // CreateTargetableRequestDelegate can only return null given a RequestDelegate passed into the other RDF.Create() overload. + Debug.Assert(targetableRequestDelegate is not null); - return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), AsReadOnlyList(factoryContext.Metadata)); + finalRequestDelegate = httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext); + } + + return CreateRequestDelegateResult(finalRequestDelegate, factoryContext.EndpointBuilder); } - private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options, Delegate? handler = null) + private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options, RequestDelegateMetadataResult? metadataResult = null, Delegate? handler = null) { - return new FactoryContext + if (metadataResult?.CachedFactoryContext is not null) + { + metadataResult.CachedFactoryContext.MetadataAlreadyInferred = true; + // The handler was not passed in to the InferMetadata call that originally created this context. + metadataResult.CachedFactoryContext.Handler = handler; + return metadataResult.CachedFactoryContext; + } + + var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; + var endpointBuilder = options?.EndpointBuilder ?? new RDFEndpointBuilder(serviceProvider); + + var factoryContext = new RequestDelegateFactoryContext { Handler = handler, - ServiceProvider = options?.ServiceProvider, - ServiceProviderIsService = options?.ServiceProvider?.GetService(), + ServiceProvider = serviceProvider, + ServiceProviderIsService = serviceProvider.GetService(), RouteParameters = options?.RouteParameterNames?.ToList(), ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, - FilterFactories = options?.EndpointFilterFactories?.ToList(), - Metadata = options?.EndpointMetadata ?? new List(), + EndpointBuilder = endpointBuilder, + MetadataAlreadyInferred = metadataResult is not null, }; + + return factoryContext; + } + + private static RequestDelegateResult CreateRequestDelegateResult(RequestDelegate finalRequestDelegate, EndpointBuilder endpointBuilder) + { + endpointBuilder.RequestDelegate = finalRequestDelegate; + return new RequestDelegateResult(finalRequestDelegate, AsReadOnlyList(endpointBuilder.Metadata)); } private static IReadOnlyList AsReadOnlyList(IList metadata) @@ -223,7 +299,11 @@ private static IReadOnlyList AsReadOnlyList(IList metadata) return new List(metadata); } - private static Func? CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext, Expression>? targetFactory = null) + private static Func? CreateTargetableRequestDelegate( + MethodInfo methodInfo, + Expression? targetExpression, + RequestDelegateFactoryContext factoryContext, + Expression>? targetFactory = null) { // Non void return type @@ -241,22 +321,18 @@ private static IReadOnlyList AsReadOnlyList(IList metadata) // return default; // } - // CreateArguments will add metadata inferred from parameter details - var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext); - var returnType = methodInfo.ReturnType; - factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments); - - // Add metadata provided by the delegate return type and parameter types next, this will be more specific than inferred metadata from above - AddTypeProvidedMetadata(methodInfo, - factoryContext.Metadata, - factoryContext.ServiceProvider, - CollectionsMarshal.AsSpan(factoryContext.Parameters)); + // If ArgumentExpressions is not null here, it's guaranteed we have already inferred metadata and we can reuse a lot of work. + // The converse is not true. Metadata may have already been inferred even if ArgumentExpressions is null, but metadata + // inference is skipped internally if necessary. + factoryContext.ArgumentExpressions ??= CreateArgumentsAndInferMetadata(methodInfo, factoryContext); + factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, factoryContext.ArgumentExpressions); EndpointFilterDelegate? filterPipeline = null; + var returnType = methodInfo.ReturnType; // If there are filters registered on the route handler, then we update the method call and // return type associated with the request to allow for the filter invocation pipeline. - if (factoryContext.FilterFactories is { Count: > 0 }) + if (factoryContext.EndpointBuilder.FilterFactories.Count > 0) { filterPipeline = CreateFilterPipeline(methodInfo, targetExpression, factoryContext, targetFactory); @@ -270,7 +346,7 @@ private static IReadOnlyList AsReadOnlyList(IList metadata) new[] { InvokedFilterContextExpr }, Expression.Assign( InvokedFilterContextExpr, - CreateEndpointFilterInvocationContextBase(factoryContext)), + CreateEndpointFilterInvocationContextBase(factoryContext, factoryContext.ArgumentExpressions)), Expression.Invoke(invokePipeline, InvokedFilterContextExpr) ); } @@ -279,11 +355,7 @@ private static IReadOnlyList AsReadOnlyList(IList metadata) // return null for plain RequestDelegates that have not been modified by filters so we can just pass back the original RequestDelegate. if (filterPipeline is null && factoryContext.Handler is RequestDelegate) { - // Make sure we're still not handling a return value. - if (!returnType.IsGenericType || returnType.GetGenericTypeDefinition() != typeof(Task<>)) - { - return null; - } + return null; } var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ? @@ -298,9 +370,26 @@ private static IReadOnlyList AsReadOnlyList(IList metadata) return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); } - private static EndpointFilterDelegate? CreateFilterPipeline(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext, Expression>? targetFactory) + private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInfo, RequestDelegateFactoryContext factoryContext) + { + // Add any default accepts metadata. This does a lot of reflection and expression tree building, so the results are cached in RequestDelegateFactoryOptions.FactoryContext + // For later reuse in Create(). + var args = CreateArguments(methodInfo.GetParameters(), factoryContext); + + if (!factoryContext.MetadataAlreadyInferred) + { + // Add metadata provided by the delegate return type and parameter types next, this will be more specific than inferred metadata from above + EndpointMetadataPopulator.PopulateMetadata(methodInfo, + factoryContext.EndpointBuilder, + factoryContext.Parameters); + } + + return args; + } + + private static EndpointFilterDelegate? CreateFilterPipeline(MethodInfo methodInfo, Expression? targetExpression, RequestDelegateFactoryContext factoryContext, Expression>? targetFactory) { - Debug.Assert(factoryContext.FilterFactories is not null); + Debug.Assert(factoryContext.EndpointBuilder.FilterFactories.Count > 0); // httpContext.Response.StatusCode >= 400 // ? Task.CompletedTask // : { @@ -339,16 +428,17 @@ targetExpression is null CompletedValueTaskExpr, handlerInvocation), FilterContextExpr).Compile(); - var routeHandlerContext = new EndpointFilterFactoryContext( - methodInfo, - factoryContext.Metadata, - factoryContext.ServiceProvider ?? EmptyServiceProvider.Instance); + var routeHandlerContext = new EndpointFilterFactoryContext + { + MethodInfo = methodInfo, + ApplicationServices = factoryContext.EndpointBuilder.ApplicationServices, + }; var initialFilteredInvocation = filteredInvocation; - for (var i = factoryContext.FilterFactories.Count - 1; i >= 0; i--) + for (var i = factoryContext.EndpointBuilder.FilterFactories.Count - 1; i >= 0; i--) { - var currentFilterFactory = factoryContext.FilterFactories[i]; + var currentFilterFactory = factoryContext.EndpointBuilder.FilterFactories[i]; filteredInvocation = currentFilterFactory(routeHandlerContext, filteredInvocation); } @@ -428,7 +518,7 @@ private static Expression MapHandlerReturnTypeToValueTask(Expression methodCall, return ExecuteAwaited(task); } - private static Expression CreateEndpointFilterInvocationContextBase(FactoryContext factoryContext) + private static Expression CreateEndpointFilterInvocationContextBase(RequestDelegateFactoryContext factoryContext, Expression[] arguments) { // In the event that a constructor matching the arity of the // provided parameters is not found, we fall back to using the @@ -447,9 +537,9 @@ private static Expression CreateEndpointFilterInvocationContextBase(FactoryConte return fallbackConstruction; } - var arguments = new Expression[factoryContext.ArgumentExpressions.Length + 1]; - arguments[0] = HttpContextExpr; - factoryContext.ArgumentExpressions.CopyTo(arguments, 1); + var expandedArguments = new Expression[arguments.Length + 1]; + expandedArguments[0] = HttpContextExpr; + arguments.CopyTo(expandedArguments, 1); var constructorType = factoryContext.ArgumentTypes?.Length switch { @@ -476,69 +566,14 @@ private static Expression CreateEndpointFilterInvocationContextBase(FactoryConte } // new EndpointFilterInvocationContext(httpContext, name_local, int_local); - return Expression.New(constructor, arguments); + return Expression.New(constructor, expandedArguments); } // new EndpointFilterInvocationContext(httpContext, (object)name_local, (object)int_local); return fallbackConstruction; } - private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList metadata, IServiceProvider? services, ReadOnlySpan parameters) - { - object?[]? invokeArgs = null; - - // Get metadata from parameter types - foreach (var parameter in parameters) - { - if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointParameterMetadataProvider - var parameterContext = new EndpointParameterMetadataContext(parameter, metadata, services ?? EmptyServiceProvider.Instance); - invokeArgs ??= new object[1]; - invokeArgs[0] = parameterContext; - PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - - if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointMetadataProvider - var context = new EndpointMetadataContext(methodInfo, metadata, services ?? EmptyServiceProvider.Instance); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - } - - // Get metadata from return type - var returnType = methodInfo.ReturnType; - if (AwaitableInfo.IsTypeAwaitable(returnType, out var awaitableInfo)) - { - returnType = awaitableInfo.ResultType; - } - - if (returnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType)) - { - // Return type implements IEndpointMetadataProvider - var context = new EndpointMetadataContext(methodInfo, metadata, services ?? EmptyServiceProvider.Instance); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(returnType).Invoke(null, invokeArgs); - } - } - - private static void PopulateMetadataForParameter(EndpointParameterMetadataContext parameterContext) - where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameterContext); - } - - private static void PopulateMetadataForEndpoint(EndpointMetadataContext context) - where T : IEndpointMetadataProvider - { - T.PopulateMetadata(context); - } - - private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext) + private static Expression[] CreateArguments(ParameterInfo[]? parameters, RequestDelegateFactoryContext factoryContext) { if (parameters is null || parameters.Length == 0) { @@ -548,11 +583,10 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory var args = new Expression[parameters.Length]; factoryContext.ArgumentTypes = new Type[parameters.Length]; - factoryContext.ArgumentExpressions = new Expression[parameters.Length]; factoryContext.BoxedArgs = new Expression[parameters.Length]; factoryContext.Parameters = new List(parameters); - var hasFilters = factoryContext.FilterFactories is { Count: > 0 }; + var hasFilters = factoryContext.EndpointBuilder.FilterFactories.Count > 0; for (var i = 0; i < parameters.Length; i++) { @@ -580,7 +614,6 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory } factoryContext.ArgumentTypes[i] = parameters[i].ParameterType; - factoryContext.ArgumentExpressions[i] = args[i]; factoryContext.BoxedArgs[i] = Expression.Convert(args[i], typeof(object)); } @@ -605,7 +638,7 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory return args; } - private static Expression CreateArgument(ParameterInfo parameter, FactoryContext factoryContext) + private static Expression CreateArgument(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { if (parameter.Name is null) { @@ -790,7 +823,7 @@ target is null ? // If we're calling TryParse or validating parameter optionality and // wasParamCheckFailure indicates it failed, set a 400 StatusCode instead of calling the method. - private static Expression CreateParamCheckingResponseWritingMethodCall(Type returnType, FactoryContext factoryContext) + private static Expression CreateParamCheckingResponseWritingMethodCall(Type returnType, RequestDelegateFactoryContext factoryContext) { // { // string tempSourceString; @@ -842,7 +875,7 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu // If filters have been registered, we set the `wasParamCheckFailure` property // but do not return from the invocation to allow the filters to run. - if (factoryContext.FilterFactories is { Count: > 0 }) + if (factoryContext.EndpointBuilder.FilterFactories.Count > 0) { // if (wasParamCheckFailure) // { @@ -999,7 +1032,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } } - private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) + private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, RequestDelegateFactoryContext factoryContext) { if (factoryContext.JsonRequestBodyParameter is null && !factoryContext.ReadForm) { @@ -1040,7 +1073,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } } - private static Func HandleRequestBodyAndCompileRequestDelegateForJson(Expression responseWritingMethodCall, FactoryContext factoryContext) + private static Func HandleRequestBodyAndCompileRequestDelegateForJson(Expression responseWritingMethodCall, RequestDelegateFactoryContext factoryContext) { Debug.Assert(factoryContext.JsonRequestBodyParameter is not null, "factoryContext.JsonRequestBodyParameter is null for a JSON body."); @@ -1164,7 +1197,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, private static Func HandleRequestBodyAndCompileRequestDelegateForForm( Expression responseWritingMethodCall, - FactoryContext factoryContext) + RequestDelegateFactoryContext factoryContext) { Debug.Assert(factoryContext.FirstFormRequestBodyParameter is not null, "factoryContext.FirstFormRequestBodyParameter is null for a form body."); @@ -1278,7 +1311,7 @@ private static Expression GetValueFromProperty(MemberExpression sourceExpression return Expression.Convert(indexExpression, returnType ?? typeof(string)); } - private static Expression BindParameterFromProperties(ParameterInfo parameter, FactoryContext factoryContext) + private static Expression BindParameterFromProperties(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { var parameterType = parameter.ParameterType; var isNullable = Nullable.GetUnderlyingType(parameterType) != null || @@ -1348,7 +1381,7 @@ private static Expression BindParameterFromProperties(ParameterInfo parameter, F return argumentExpression; } - private static Expression BindParameterFromService(ParameterInfo parameter, FactoryContext factoryContext) + private static Expression BindParameterFromService(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { var isOptional = IsOptionalParameter(parameter, factoryContext); @@ -1359,7 +1392,7 @@ private static Expression BindParameterFromService(ParameterInfo parameter, Fact return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); } - private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext, string source) + private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, RequestDelegateFactoryContext factoryContext, string source) { if (parameter.ParameterType == typeof(string) || parameter.ParameterType == typeof(string[]) || parameter.ParameterType == typeof(StringValues) || parameter.ParameterType == typeof(StringValues?)) @@ -1589,7 +1622,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres private static Expression BindParameterFromExpression( ParameterInfo parameter, Expression valueExpression, - FactoryContext factoryContext, + RequestDelegateFactoryContext factoryContext, string source) { var nullability = factoryContext.NullabilityContext.Create(parameter); @@ -1658,7 +1691,7 @@ private static Expression BindParameterFromExpression( Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType))); } - private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, FactoryContext factoryContext, string source) => + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, RequestDelegateFactoryContext factoryContext, string source) => BindParameterFromValue(parameter, GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType)), factoryContext, source); private static Type? GetExpressionType(Type type) => @@ -1667,14 +1700,14 @@ private static Expression BindParameterFromProperty(ParameterInfo parameter, Mem type == typeof(StringValues?) ? typeof(StringValues?) : null; - private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) + private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, RequestDelegateFactoryContext factoryContext) { var routeValue = GetValueFromProperty(RouteValuesExpr, RouteValuesIndexerProperty, key); var queryValue = GetValueFromProperty(QueryExpr, QueryIndexerProperty, key); return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext, "route or query string"); } - private static Expression BindParameterFromBindAsync(ParameterInfo parameter, FactoryContext factoryContext) + private static Expression BindParameterFromBindAsync(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { // We reference the boundValues array by parameter index here var isOptional = IsOptionalParameter(parameter, factoryContext); @@ -1717,17 +1750,19 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa return Expression.Convert(boundValueExpr, parameter.ParameterType); } - private static void InsertInferredAcceptsMetadata(FactoryContext factoryContext, Type type, string[] contentTypes) + private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext factoryContext, Type type, string[] contentTypes) { - // Insert the automatically-inferred AcceptsMetadata at the beginning of the list to give it the lowest precedence. - // It really doesn't makes sense for this metadata to be overridden, but we're preserving the old behavior out of an abundance of caution. - // I suspect most filters and metadata providers will just add their metadata to the end of the list. - factoryContext.Metadata.Insert(0, new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes)); + if (factoryContext.MetadataAlreadyInferred) + { + return; + } + + factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes)); } private static Expression BindParameterFromFormFiles( ParameterInfo parameter, - FactoryContext factoryContext) + RequestDelegateFactoryContext factoryContext) { if (factoryContext.FirstFormRequestBodyParameter is null) { @@ -1739,7 +1774,7 @@ private static Expression BindParameterFromFormFiles( // Do not duplicate the metadata if there are multiple form parameters if (!factoryContext.ReadForm) { - InsertInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); + AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } factoryContext.ReadForm = true; @@ -1750,7 +1785,7 @@ private static Expression BindParameterFromFormFiles( private static Expression BindParameterFromFormFile( ParameterInfo parameter, string key, - FactoryContext factoryContext, + RequestDelegateFactoryContext factoryContext, string trackedParameterSource) { if (factoryContext.FirstFormRequestBodyParameter is null) @@ -1763,7 +1798,7 @@ private static Expression BindParameterFromFormFile( // Do not duplicate the metadata if there are multiple form parameters if (!factoryContext.ReadForm) { - InsertInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); + AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } factoryContext.ReadForm = true; @@ -1773,7 +1808,7 @@ private static Expression BindParameterFromFormFile( return BindParameterFromExpression(parameter, valueExpression, factoryContext, "form file"); } - private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) + private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, RequestDelegateFactoryContext factoryContext) { if (factoryContext.JsonRequestBodyParameter is not null) { @@ -1793,7 +1828,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al factoryContext.JsonRequestBodyParameter = parameter; factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional; - InsertInferredAcceptsMetadata(factoryContext, parameter.ParameterType, DefaultAcceptsContentType); + AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, DefaultAcceptsContentType); if (!factoryContext.AllowEmptyRequestBody) { @@ -1857,7 +1892,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al return Expression.Convert(BodyValueExpr, parameter.ParameterType); } - private static bool IsOptionalParameter(ParameterInfo parameter, FactoryContext factoryContext) + private static bool IsOptionalParameter(ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { if (parameter is PropertyAsParameterInfo argument) { @@ -2096,50 +2131,6 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex { await EnsureRequestResultNotNull(result).ExecuteAsync(httpContext); } - - private sealed class FactoryContext - { - // Options - // Handler could be null if the MethodInfo overload of RDF.Create is used, but that doesn't matter because this is - // only referenced to optimize certain cases where a RequestDelegate is the handler and filters don't modify it. - public Delegate? Handler { get; init; } - public IServiceProvider? ServiceProvider { get; init; } - public IServiceProviderIsService? ServiceProviderIsService { get; init; } - public List? RouteParameters { get; init; } - public bool ThrowOnBadRequest { get; init; } - public bool DisableInferredFromBody { get; init; } - - // Temporary State - public ParameterInfo? JsonRequestBodyParameter { get; set; } - public bool AllowEmptyRequestBody { get; set; } - - public bool UsingTempSourceString { get; set; } - public List ExtraLocals { get; } = new(); - public List ParamCheckExpressions { get; } = new(); - public List>> ParameterBinders { get; } = new(); - - public Dictionary TrackedParameters { get; } = new(); - public bool HasMultipleBodyParameters { get; set; } - public bool HasInferredBody { get; set; } - - public IList Metadata { get; init; } = default!; - - public NullabilityInfoContext NullabilityContext { get; } = new(); - - public bool ReadForm { get; set; } - public ParameterInfo? FirstFormRequestBodyParameter { get; set; } - // Properties for constructing and managing filters - public List ContextArgAccess { get; } = new(); - public Expression? MethodCall { get; set; } - public Type[] ArgumentTypes { get; set; } = Array.Empty(); - public Expression[] ArgumentExpressions { get; set; } = Array.Empty(); - public Expression[] BoxedArgs { get; set; } = Array.Empty(); - public List>? FilterFactories { get; init; } - public bool FilterFactoriesHaveRunWithoutModifyingPerRequestBehavior { get; set; } - - public List Parameters { get; set; } = new(); - } - private static class RequestDelegateFactoryConstants { public const string RouteAttribute = "Route (Attribute)"; @@ -2324,7 +2315,7 @@ private static void SetPlaintextContentType(HttpContext httpContext) httpContext.Response.ContentType ??= "text/plain; charset=utf-8"; } - private static string BuildErrorMessageForMultipleBodyParameters(FactoryContext factoryContext) + private static string BuildErrorMessageForMultipleBodyParameters(RequestDelegateFactoryContext factoryContext) { var errorMessage = new StringBuilder(); errorMessage.AppendLine("Failure to infer one or more parameters."); @@ -2341,7 +2332,7 @@ private static string BuildErrorMessageForMultipleBodyParameters(FactoryContext return errorMessage.ToString(); } - private static string BuildErrorMessageForInferredBodyParameter(FactoryContext factoryContext) + private static string BuildErrorMessageForInferredBodyParameter(RequestDelegateFactoryContext factoryContext) { var errorMessage = new StringBuilder(); errorMessage.AppendLine("Body was inferred but the method does not allow inferred body parameters."); @@ -2358,7 +2349,7 @@ private static string BuildErrorMessageForInferredBodyParameter(FactoryContext f return errorMessage.ToString(); } - private static string BuildErrorMessageForFormAndJsonBodyParameters(FactoryContext factoryContext) + private static string BuildErrorMessageForFormAndJsonBodyParameters(RequestDelegateFactoryContext factoryContext) { var errorMessage = new StringBuilder(); errorMessage.AppendLine("An action cannot use both form and JSON body parameters."); @@ -2372,7 +2363,7 @@ private static string BuildErrorMessageForFormAndJsonBodyParameters(FactoryConte return errorMessage.ToString(); } - private static void FormatTrackedParameters(FactoryContext factoryContext, StringBuilder errorMessage) + private static void FormatTrackedParameters(RequestDelegateFactoryContext factoryContext, StringBuilder errorMessage) { foreach (var kv in factoryContext.TrackedParameters) { @@ -2398,10 +2389,22 @@ public Task ExecuteAsync(HttpContext httpContext) } } - private sealed class EmptyServiceProvider : IServiceProvider + private sealed class RDFEndpointBuilder : EndpointBuilder { - public static IServiceProvider Instance { get; } = new EmptyServiceProvider(); + public RDFEndpointBuilder(IServiceProvider applicationServices) + { + ApplicationServices = applicationServices; + } + public override Endpoint Build() + { + throw new NotSupportedException(); + } + } + + private sealed class EmptyServiceProvider : IServiceProvider + { + public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider(); public object? GetService(Type serviceType) => null; } } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs new file mode 100644 index 000000000000..65b9d7b0f964 --- /dev/null +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Http; + +internal sealed class RequestDelegateFactoryContext +{ + // Options + public required IServiceProvider ServiceProvider { get; init; } + public required IServiceProviderIsService? ServiceProviderIsService { get; init; } + public required List? RouteParameters { get; init; } + public required bool ThrowOnBadRequest { get; init; } + public required bool DisableInferredFromBody { get; init; } + public required EndpointBuilder EndpointBuilder { get; init; } + + // Handler could be null if the MethodInfo overload of RDF.Create is used, but that doesn't matter because this is + // only referenced to optimize certain cases where a RequestDelegate is the handler and filters don't modify it. + public Delegate? Handler { get; set; } + + // Temporary State + + // This indicates whether we're currently in RDF.Create() with a non-null RequestDelegateResult. + // This is settable, because if this context is cached we need to set it to true after it's created. + // But it's still possible this should be initialized to true initially, so we're making it required. + // In theory, someone could construct their own RequestDelegateResult without a cached context. + public required bool MetadataAlreadyInferred { get; set; } + + public ParameterInfo? JsonRequestBodyParameter { get; set; } + public bool AllowEmptyRequestBody { get; set; } + + public bool UsingTempSourceString { get; set; } + public List ExtraLocals { get; } = new(); + public List ParamCheckExpressions { get; } = new(); + public List>> ParameterBinders { get; } = new(); + + public Dictionary TrackedParameters { get; } = new(); + public bool HasMultipleBodyParameters { get; set; } + public bool HasInferredBody { get; set; } + + public NullabilityInfoContext NullabilityContext { get; } = new(); + + public bool ReadForm { get; set; } + public ParameterInfo? FirstFormRequestBodyParameter { get; set; } + // Properties for constructing and managing filters + public List ContextArgAccess { get; } = new(); + public Expression? MethodCall { get; set; } + public Type[] ArgumentTypes { get; set; } = Array.Empty(); + public Expression[]? ArgumentExpressions { get; set; } + public Expression[] BoxedArgs { get; set; } = Array.Empty(); + public bool FilterFactoriesHaveRunWithoutModifyingPerRequestBehavior { get; set; } + + public List Parameters { get; set; } = new(); +} diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index a3f05d9db8a3..a11cba424330 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -34,25 +34,18 @@ public sealed class RequestDelegateFactoryOptions public bool DisableInferBodyFromParameters { get; init; } /// - /// The list of filters that must run in the pipeline for a given route handler. - /// - public IReadOnlyList>? EndpointFilterFactories { get; init; } - - /// - /// The mutable initial endpoint metadata to add as part of the creation of the . In most cases, - /// this should come from . + /// The mutable used to assist in the creation of the . + /// This is primarily used to run and populate inferred . + /// The must be . After the call to , + /// will be the same as . /// /// - /// This metadata will be included in before most metadata inferred during creation of the - /// and before any metadata provided by types in the delegate signature that implement - /// or . The exception to this general rule is the + /// Any metadata already in will be included in before + /// most metadata inferred during creation of the and before any metadata provided by types in + /// the delegate signature that implement or . The exception to this general rule is the /// that infers automatically /// without any custom metadata providers which instead is inserted at the start to give it lower precedence. Custom metadata providers can choose to /// insert their metadata at the start to give lower precedence, but this is unusual. /// - public IList? EndpointMetadata { get; init; } - - // TODO: Add a RouteEndpointBuilder property and remove the EndpointMetadata property. Then do the same in RouteHandlerContext, EndpointMetadataContext - // and EndpointParameterMetadataContext. This will allow seeing the entire route pattern if the caller chooses to allow it. - // We'll probably want to add the RouteEndpointBuilder constructor without a RequestDelegate back and make it public too. + public EndpointBuilder? EndpointBuilder { get; init; } } diff --git a/src/Http/Http.Extensions/src/RequestDelegateMetadataResult.cs b/src/Http/Http.Extensions/src/RequestDelegateMetadataResult.cs new file mode 100644 index 000000000000..3ca91ac8bacf --- /dev/null +++ b/src/Http/Http.Extensions/src/RequestDelegateMetadataResult.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http; + +/// +/// The metadata inferred by . +/// will be automatically populated with this metadata if provided. +/// If this is passed to , +/// it will not repeat metadata inference. Any metadata that would be inferred should already be stored in the EndpointBuilder. +/// +public sealed class RequestDelegateMetadataResult +{ + /// + /// Gets endpoint metadata inferred from creating the . If a non-null + /// RequestDelegateFactoryOptions.EndpointMetadata list was passed in, this will be the same instance. + /// + public required IReadOnlyList EndpointMetadata { get; init; } + + // This internal cached context avoids redoing unnecessary reflection in Create that was already done in InferMetadata. + // InferMetadata currently does more work than it needs to building up expression trees, but the expectation is that InferMetadata will usually be followed by Create. + internal RequestDelegateFactoryContext? CachedFactoryContext { get; set; } +} diff --git a/src/Http/Http.Extensions/test/EndpointMetadataContextTests.cs b/src/Http/Http.Extensions/test/EndpointMetadataContextTests.cs deleted file mode 100644 index 3fa0a9a143c6..000000000000 --- a/src/Http/Http.Extensions/test/EndpointMetadataContextTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Metadata; - -namespace Microsoft.AspNetCore.Http.Extensions.Tests; - -public class EndpointMetadataContextTests -{ - [Fact] - public void EndpointMetadataContext_Ctor_ThrowsArgumentNullException_WhenMethodInfoIsNull() - { - Assert.Throws("method", () => new EndpointMetadataContext(null, new List(), null)); - } - - [Fact] - public void EndpointMetadataContext_Ctor_ThrowsArgumentNullException_WhenMetadataIsNull() - { - Delegate handler = (int id) => { }; - var method = handler.GetMethodInfo(); - - Assert.Throws("endpointMetadata", () => new EndpointMetadataContext(method, null, null)); - } - - [Fact] - public void EndpointMetadataContext_Ctor_ThrowsArgumentNullException_WhenApplicationServicesAreNull() - { - Delegate handler = (int id) => { }; - var method = handler.GetMethodInfo(); - - Assert.Throws("applicationServices", () => new EndpointMetadataContext(method, new List(), null)); - } - - [Fact] - public void EndpointParameterMetadataContext_Ctor_ThrowsArgumentNullException_WhenParameterInfoIsNull() - { - Assert.Throws("parameter", () => new EndpointParameterMetadataContext(null, new List(), null)); - } - - [Fact] - public void EndpointParameterMetadataContext_Ctor_ThrowsArgumentNullException_WhenMetadataIsNull() - { - Delegate handler = (int id) => { }; - var parameter = handler.GetMethodInfo().GetParameters()[0]; - - Assert.Throws("endpointMetadata", () => new EndpointParameterMetadataContext(parameter, null, null)); - } - - [Fact] - public void EndpointParameterMetadataContext_Ctor_ThrowsArgumentNullException_WhenApplicationServicesAreNull() - { - Delegate handler = (int id) => { }; - var parameter = handler.GetMethodInfo().GetParameters()[0]; - - Assert.Throws("applicationServices", () => new EndpointParameterMetadataContext(parameter, new List(), null)); - } -} diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 3517fe81b4f8..63f4e1d0c3c0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4,6 +4,7 @@ #nullable enable using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO.Pipelines; using System.Linq.Expressions; @@ -18,10 +19,12 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; @@ -5164,14 +5167,14 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { context.Arguments[0] = context.Arguments[0] != null ? $"{((string)context.Arguments[0]!)}Prefix" : "NULL"; return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5196,13 +5199,13 @@ public async Task RequestDelegateFactory_InvokesFilters_OnDelegateWithTarget() // Act var factoryResult = RequestDelegateFactory.Create((string name) => $"Hello, {name}!", new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5238,13 +5241,13 @@ public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithNullTarg // Act var factoryResult = RequestDelegateFactory.Create(methodInfo!, null, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5281,13 +5284,13 @@ public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithProvided }; var factoryResult = RequestDelegateFactory.Create(methodInfo!, targetFactory, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5318,7 +5321,7 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() { + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { if (context.HttpContext.Response.StatusCode == 400) @@ -5327,7 +5330,7 @@ string HelloName(string name) } return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5364,7 +5367,7 @@ string HelloName(string name, int age) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5379,7 +5382,7 @@ string HelloName(string name, int age) } return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5412,7 +5415,7 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => { @@ -5428,7 +5431,7 @@ string HelloName(string name) return "Is not an int."; }; }, - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5439,7 +5442,7 @@ string HelloName(string name) } [Fact] - public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatUsesEndpointMetadata() + public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatReadsEndpointMetadata() { // Arrange string HelloName(IFormFileCollection formFiles) @@ -5467,26 +5470,32 @@ string HelloName(IFormFileCollection formFiles) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => { - var acceptsMetadata = routeHandlerContext.EndpointMetadata.OfType(); - var contentType = acceptsMetadata.SingleOrDefault()?.ContentTypes.SingleOrDefault(); + string? contentType = null; return async (context) => { + contentType ??= context.HttpContext.GetEndpoint()?.Metadata.GetMetadata()?.ContentTypes.SingleOrDefault(); + if (contentType == "multipart/form-data") { return "I see you expect a form."; } + return await next(context); }; }, - } + }), }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + + var builder = new RouteEndpointBuilder(factoryResult.RequestDelegate, RoutePatternFactory.Parse("/"), order: 0); + ((List)builder.Metadata).AddRange(factoryResult.EndpointMetadata); + httpContext.Features.Set(new EndpointFeature { Endpoint = builder.Build() }); + + await factoryResult.RequestDelegate(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); @@ -5518,7 +5527,7 @@ string PrintTodo(Todo todo) // Act var factoryResult = RequestDelegateFactory.Create(PrintTodo, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5527,7 +5536,7 @@ string PrintTodo(Todo todo) context.Arguments[0] = originalTodo; return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5559,7 +5568,7 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5570,7 +5579,7 @@ string HelloName(string name) } return previousResult; } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5602,7 +5611,7 @@ string HelloName(string name) // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5619,7 +5628,7 @@ string HelloName(string name) context.Arguments[0] = newValue; return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5671,13 +5680,13 @@ public async Task CanInvokeFilter_OnTaskOfTReturningHandler(Delegate @delegate) // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5729,13 +5738,13 @@ public async Task CanInvokeFilter_OnValueTaskOfTReturningHandler(Delegate @deleg // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5794,13 +5803,13 @@ public async Task CanInvokeFilter_OnVoidReturningHandler(Delegate @delegate) // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5828,13 +5837,13 @@ async Task HandlerWithTaskAwait(HttpContext c) // Act var factoryResult = RequestDelegateFactory.Create(HandlerWithTaskAwait, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; @@ -5895,13 +5904,13 @@ public async Task CanInvokeFilter_OnHandlerReturningTasksOfStruct(Delegate @dele // Act var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5931,7 +5940,7 @@ string HelloName(int? one, string? two, int? three, string? four, int? five, boo // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5939,7 +5948,7 @@ string HelloName(int? one, string? two, int? three, string? four, int? five, boo Assert.Equal(11, context.Arguments.Count); return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5962,7 +5971,7 @@ string HelloName() // Act var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => async (context) => { @@ -5970,7 +5979,7 @@ string HelloName() Assert.Equal(0, context.Arguments.Count); return await next(context); } - } + }), }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -5996,7 +6005,7 @@ public void Create_DoesNotAddAnythingBefore_ThePassedInEndpointMetadata() // Arrange var @delegate = (AddsCustomParameterMetadataBindable param1) => { }; var customMetadata = new CustomEndpointMetadata(); - var options = new RequestDelegateFactoryOptions { EndpointMetadata = new List { customMetadata } }; + var options = new RequestDelegateFactoryOptions { EndpointBuilder = CreateEndpointBuilder(new List { customMetadata }) }; // Act var result = RequestDelegateFactory.Create(@delegate, options); @@ -6097,10 +6106,10 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin var @delegate = () => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6109,7 +6118,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added - Assert.Contains(result.EndpointMetadata, m => m is DefaultMetadataCountMetadata { Count: 1 }); + Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] @@ -6119,10 +6128,10 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromTaskWrappedReturnTypes var @delegate = () => Task.FromResult(new CountsDefaultEndpointMetadataResult()); var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6131,7 +6140,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromTaskWrappedReturnTypes // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added - Assert.Contains(result.EndpointMetadata, m => m is DefaultMetadataCountMetadata { Count: 1 }); + Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] @@ -6141,10 +6150,10 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromValueTaskWrappedReturn var @delegate = () => ValueTask.FromResult(new CountsDefaultEndpointMetadataResult()); var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6153,7 +6162,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromValueTaskWrappedReturn // Assert Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added - Assert.Contains(result.EndpointMetadata, m => m is DefaultMetadataCountMetadata { Count: 1 }); + Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } [Fact] @@ -6163,10 +6172,10 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6184,10 +6193,10 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6205,10 +6214,10 @@ public void Create_CombinesPropertiesAsParameterMetadata_AndTopLevelParameter() var @delegate = ([AsParameters] AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6228,10 +6237,10 @@ public void Create_CombinesAllMetadata_InCorrectOrder() var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { - EndpointMetadata = new List + EndpointBuilder = CreateEndpointBuilder(new List { new CustomEndpointMetadata { Source = MetadataSource.Caller } - } + }), }; // Act @@ -6239,16 +6248,106 @@ public void Create_CombinesAllMetadata_InCorrectOrder() // Assert Assert.Collection(result.EndpointMetadata, + // Initial metadata from RequestDelegateFactoryOptions.EndpointBuilder. If the caller want to override inferred metadata, + // They need to call InferMetadata first, then add the overriding metadata, and then call Create with InferMetadata's result. + // This is demonstrated in the following tests. + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller }), // Inferred AcceptsMetadata from RDF for complex type m => Assert.True(m is AcceptsMetadata am && am.RequestType == typeof(AddsCustomParameterMetadata)), - // Initial metadata from RequestDelegateFactoryOptions.InitialEndpointMetadata - m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller }), // Metadata provided by parameters implementing IEndpointParameterMetadataProvider m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), // Metadata provided by parameters implementing IEndpointMetadataProvider m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter }), // Metadata provided by return type implementing IEndpointMetadataProvider - m => Assert.True(m is DefaultMetadataCountMetadata { Count: 4 })); + m => Assert.True(m is MetadataCountMetadata { Count: 4 })); + } + + [Fact] + public void Create_FlowsRoutePattern_ToMetadataProvider() + { + // Arrange + var @delegate = (AddsRoutePatternMetadata param1) => { }; + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/test/pattern"), order: 0); + var options = new RequestDelegateFactoryOptions + { + EndpointBuilder = builder, + }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is RoutePatternMetadata { RoutePattern: "/test/pattern" }); + } + + [Fact] + public void Create_DoesNotInferMetadata_GivenManuallyConstructedMetadataResult() + { + var invokeCount = 0; + + // Arrange + var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => + { + invokeCount++; + return new CountsDefaultEndpointMetadataResult(); + }; + + var options = new RequestDelegateFactoryOptions + { + EndpointBuilder = CreateEndpointBuilder(), + }; + var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List() }; + var httpContext = CreateHttpContext(); + + // An empty object should deserialize to AddsCustomParameterMetadata since it has no required properties. + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new object()); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; + + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + // Act + var result = RequestDelegateFactory.Create(@delegate, options, metadataResult); + + // Assert + Assert.Empty(result.EndpointMetadata); + Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata); + + // Make extra sure things are running as expected, as this non-InferMetadata path is no longer exercised by RouteEndpointDataSource, + // and most of the other unit tests don't pass in a metadataResult without a cached factory context. + Assert.True(result.RequestDelegate(httpContext).IsCompletedSuccessfully); + Assert.Equal(1, invokeCount); + } + + [Fact] + public void InferMetadata_ThenCreate_CombinesAllMetadata_InCorrectOrder() + { + // Arrange + var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataResult(); + var options = new RequestDelegateFactoryOptions + { + EndpointBuilder = CreateEndpointBuilder(), + }; + + // Act + var metadataResult = RequestDelegateFactory.InferMetadata(@delegate.Method, options); + options.EndpointBuilder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Caller }); + var result = RequestDelegateFactory.Create(@delegate, options, metadataResult); + + // Assert + Assert.Collection(result.EndpointMetadata, + // Inferred AcceptsMetadata from RDF for complex type + m => Assert.True(m is AcceptsMetadata am && am.RequestType == typeof(AddsCustomParameterMetadata)), + // Metadata provided by parameters implementing IEndpointParameterMetadataProvider + m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), + // Metadata provided by parameters implementing IEndpointMetadataProvider + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter }), + // Metadata provided by return type implementing IEndpointMetadataProvider + m => Assert.True(m is MetadataCountMetadata { Count: 3 }), + // Entry-specific metadata added after a call to InferMetadata + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller })); } [Fact] @@ -6351,31 +6450,44 @@ public void Create_SetsApplicationServices_OnEndpointParameterMetadataContext() public void Create_ReturnsSameRequestDelegatePassedIn_IfNotModifiedByFilters() { RequestDelegate initialRequestDelegate = static (context) => Task.CompletedTask; - var filter1Tag = new TagsAttribute("filter1"); - var filter2Tag = new TagsAttribute("filter2"); + var invokeCount = 0; RequestDelegateFactoryOptions options = new() { - EndpointFilterFactories = new List>() + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { (routeHandlerContext, next) => { - routeHandlerContext.EndpointMetadata.Add(filter1Tag); + invokeCount++; return next; }, (routeHandlerContext, next) => { - routeHandlerContext.EndpointMetadata.Add(filter2Tag); + invokeCount++; return next; }, - } + }), }; var result = RequestDelegateFactory.Create(initialRequestDelegate, options); Assert.Same(initialRequestDelegate, result.RequestDelegate); - Assert.Collection(result.EndpointMetadata, - m => Assert.Same(filter2Tag, m), - m => Assert.Same(filter1Tag, m)); + Assert.Equal(2, invokeCount); + } + + [Fact] + public void Create_Populates_EndpointBuilderWithRequestDelegateAndMetadata() + { + RequestDelegate requestDelegate = static context => Task.CompletedTask; + + RequestDelegateFactoryOptions options = new() + { + EndpointBuilder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), order: 0), + }; + + var result = RequestDelegateFactory.Create(requestDelegate, options); + + Assert.Same(options.EndpointBuilder.RequestDelegate, result.RequestDelegate); + Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata); } private DefaultHttpContext CreateHttpContext() @@ -6394,15 +6506,32 @@ private DefaultHttpContext CreateHttpContext() }; } + private EndpointBuilder CreateEndpointBuilder(IEnumerable? metadata = null) + { + var builder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), 0); + if (metadata is not null) + { + ((List)builder.Metadata).AddRange(metadata); + } + return builder; + } + + private EndpointBuilder CreateEndpointBuilderFromFilterFactories(IEnumerable> filterFactories) + { + var builder = new RouteEndpointBuilder(null, RoutePatternFactory.Parse("/"), 0); + ((List>)builder.FilterFactories).AddRange(filterFactories); + return builder; + } + private record MetadataService; private class AccessesServicesMetadataResult : IResult, IEndpointMetadataProvider { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - if (context.ApplicationServices?.GetRequiredService() is { } metadataService) + if (builder.ApplicationServices.GetRequiredService() is { } metadataService) { - context.EndpointMetadata.Add(metadataService); + builder.Metadata.Add(metadataService); } } @@ -6414,11 +6543,11 @@ private class AccessesServicesMetadataBinder : IEndpointMetadataProvider public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => new(new AccessesServicesMetadataBinder()); - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - if (context.ApplicationServices?.GetRequiredService() is { } metadataService) + if (builder.ApplicationServices.GetRequiredService() is { } metadataService) { - context.EndpointMetadata.Add(metadataService); + builder.Metadata.Add(metadataService); } } } @@ -6433,9 +6562,9 @@ private class Attribute2 : Attribute private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IResult { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); @@ -6443,7 +6572,7 @@ public static void PopulateMetadata(EndpointMetadataContext context) private class AddsNoEndpointMetadataResult : IEndpointMetadataProvider, IResult { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { } @@ -6453,27 +6582,27 @@ public static void PopulateMetadata(EndpointMetadataContext context) private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, IResult { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - var defaultMetadataCount = context.EndpointMetadata?.Count; - context.EndpointMetadata?.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount ?? 0 }); + var currentMetadataCount = builder.Metadata.Count; + builder.Metadata.Add(new MetadataCountMetadata { Count = currentMetadataCount }); } - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; } private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider { - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { - if (parameterContext.EndpointMetadata is not null) + if (builder.Metadata is not null) { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + for (int i = builder.Metadata.Count - 1; i >= 0; i--) { - var metadata = parameterContext.EndpointMetadata[i]; + var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { - parameterContext.EndpointMetadata.RemoveAt(i); + builder.Metadata.RemoveAt(i); } } } @@ -6482,16 +6611,16 @@ public static void PopulateMetadata(EndpointParameterMetadataContext parameterCo private class RemovesAcceptsMetadata : IEndpointMetadataProvider { - public static void PopulateMetadata(EndpointMetadataContext parameterContext) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - if (parameterContext.EndpointMetadata is not null) + if (builder.Metadata is not null) { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + for (int i = builder.Metadata.Count - 1; i >= 0; i--) { - var metadata = parameterContext.EndpointMetadata[i]; + var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { - parameterContext.EndpointMetadata.RemoveAt(i); + builder.Metadata.RemoveAt(i); } } } @@ -6500,16 +6629,16 @@ public static void PopulateMetadata(EndpointMetadataContext parameterContext) private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - if (context.EndpointMetadata is not null) + if (builder.Metadata is not null) { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) + for (int i = builder.Metadata.Count - 1; i >= 0; i--) { - var metadata = context.EndpointMetadata[i]; + var metadata = builder.Metadata[i]; if (metadata is IAcceptsMetadata) { - context.EndpointMetadata.RemoveAt(i); + builder.Metadata.RemoveAt(i); } } } @@ -6520,48 +6649,79 @@ public static void PopulateMetadata(EndpointMetadataContext context) private class AddsCustomParameterMetadataAsProperty : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { - parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); + builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); } - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Property }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Property }); } } - private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + // TODO: Binding breaks if we explicitly implement IParsable. :( + // We could special-case IParsable because we have a reference to it. The check for `!method.IsAbstract` in GetStaticMethodFromHierarchy + // stops us from finding it now. But even if we did find it, we haven't implemented the correct code gen to call it for unreferenced interfaces. + // We might have to use Type.GetInterfaceMap. See previous discussion: https://github.com/dotnet/aspnetcore/pull/40926#discussion_r837781209 + // + // System.InvalidOperationException : TryParse method found on AddsCustomParameterMetadata with incorrect format. Must be a static method with format + // bool TryParse(string, IFormatProvider, out AddsCustomParameterMetadata) + // bool TryParse(string, out AddsCustomParameterMetadata) + // but found + // static Boolean TryParse(System.String, System.IFormatProvider, AddsCustomParameterMetadata ByRef) + private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider//, IParsable { public AddsCustomParameterMetadataAsProperty? Data { get; set; } - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { - parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); + builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); } - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } + + //static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out AddsCustomParameterMetadata result) + //{ + // result = new(); + // return true; + //} + + //static AddsCustomParameterMetadata IParsable.Parse(string s, IFormatProvider? provider) => throw new NotSupportedException(); } private class AddsCustomParameterMetadataBindable : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) + { + builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); + } + + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } + } - public static void PopulateMetadata(EndpointMetadataContext context) + private class AddsRoutePatternMetadata : IEndpointMetadataProvider + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); + if (builder is not RouteEndpointBuilder reb) + { + return; + } + + builder.Metadata.Add(new RoutePatternMetadata { RoutePattern = reb.RoutePattern?.RawText ?? string.Empty }); } } - private class DefaultMetadataCountMetadata + private class MetadataCountMetadata { public int Count { get; init; } } @@ -6578,6 +6738,11 @@ private class CustomEndpointMetadata public MetadataSource Source { get; init; } } + private class RoutePatternMetadata + { + public string RoutePattern { get; init; } = String.Empty; + } + private enum MetadataSource { Caller, @@ -6929,6 +7094,11 @@ public TlsConnectionFeature(X509Certificate2 clientCertificate) throw new NotImplementedException(); } } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint? Endpoint { get; set; } + } } internal static class TestExtensionResults diff --git a/src/Http/Http.Results/src/Accepted.cs b/src/Http/Http.Results/src/Accepted.cs index 42bd0b4ed9d8..0a23b209c2b6 100644 --- a/src/Http/Http.Results/src/Accepted.cs +++ b/src/Http/Http.Results/src/Accepted.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -74,10 +76,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRoute.cs b/src/Http/Http.Results/src/AcceptedAtRoute.cs index 5698f463744f..55489db2d29c 100644 --- a/src/Http/Http.Results/src/AcceptedAtRoute.cs +++ b/src/Http/Http.Results/src/AcceptedAtRoute.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -86,10 +88,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index af9a471cf2c0..c270926e5223 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -99,10 +101,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); } } diff --git a/src/Http/Http.Results/src/AcceptedOfT.cs b/src/Http/Http.Results/src/AcceptedOfT.cs index 1911881fc4cc..cc3051108d36 100644 --- a/src/Http/Http.Results/src/AcceptedOfT.cs +++ b/src/Http/Http.Results/src/AcceptedOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -96,10 +98,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status202Accepted, "application/json")); } } diff --git a/src/Http/Http.Results/src/BadRequest.cs b/src/Http/Http.Results/src/BadRequest.cs index d2bb333542fc..e27c56889ae5 100644 --- a/src/Http/Http.Results/src/BadRequest.cs +++ b/src/Http/Http.Results/src/BadRequest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,10 +46,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest)); } } diff --git a/src/Http/Http.Results/src/BadRequestOfT.cs b/src/Http/Http.Results/src/BadRequestOfT.cs index 9056667b79c1..c9d48ed8be4f 100644 --- a/src/Http/Http.Results/src/BadRequestOfT.cs +++ b/src/Http/Http.Results/src/BadRequestOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -58,10 +60,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status400BadRequest, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status400BadRequest, "application/json")); } } diff --git a/src/Http/Http.Results/src/Conflict.cs b/src/Http/Http.Results/src/Conflict.cs index 086ecd1f9ba4..5504c2cab069 100644 --- a/src/Http/Http.Results/src/Conflict.cs +++ b/src/Http/Http.Results/src/Conflict.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,10 +46,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict)); } } diff --git a/src/Http/Http.Results/src/ConflictOfT.cs b/src/Http/Http.Results/src/ConflictOfT.cs index baf16f6fc85a..f78bf8b8b76c 100644 --- a/src/Http/Http.Results/src/ConflictOfT.cs +++ b/src/Http/Http.Results/src/ConflictOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -58,10 +60,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status409Conflict, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status409Conflict, "application/json")); } } diff --git a/src/Http/Http.Results/src/Created.cs b/src/Http/Http.Results/src/Created.cs index 02091f69db8c..6c4be159c3c0 100644 --- a/src/Http/Http.Results/src/Created.cs +++ b/src/Http/Http.Results/src/Created.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -76,10 +78,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); } } diff --git a/src/Http/Http.Results/src/CreatedAtRoute.cs b/src/Http/Http.Results/src/CreatedAtRoute.cs index a3e8d34cb4af..213576e24e52 100644 --- a/src/Http/Http.Results/src/CreatedAtRoute.cs +++ b/src/Http/Http.Results/src/CreatedAtRoute.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -86,10 +88,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created)); } } diff --git a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs index 1a64b3415a20..6d543d9b98f5 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -102,10 +104,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); } } diff --git a/src/Http/Http.Results/src/CreatedOfT.cs b/src/Http/Http.Results/src/CreatedOfT.cs index b6104f30c0c6..0e8cfea4a217 100644 --- a/src/Http/Http.Results/src/CreatedOfT.cs +++ b/src/Http/Http.Results/src/CreatedOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -94,10 +96,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status201Created, "application/json")); } } diff --git a/src/Http/Http.Results/src/NoContent.cs b/src/Http/Http.Results/src/NoContent.cs index f52e0ad4bd94..75c8cfa2c1c7 100644 --- a/src/Http/Http.Results/src/NoContent.cs +++ b/src/Http/Http.Results/src/NoContent.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,10 +46,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent)); } } diff --git a/src/Http/Http.Results/src/NotFound.cs b/src/Http/Http.Results/src/NotFound.cs index 069c2d7ad62c..67df2f05af2c 100644 --- a/src/Http/Http.Results/src/NotFound.cs +++ b/src/Http/Http.Results/src/NotFound.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -43,10 +45,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound)); } } diff --git a/src/Http/Http.Results/src/NotFoundOfT.cs b/src/Http/Http.Results/src/NotFoundOfT.cs index 18a39d02f42a..f358691e1ecd 100644 --- a/src/Http/Http.Results/src/NotFoundOfT.cs +++ b/src/Http/Http.Results/src/NotFoundOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -57,10 +59,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status404NotFound, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status404NotFound, "application/json")); } } diff --git a/src/Http/Http.Results/src/Ok.cs b/src/Http/Http.Results/src/Ok.cs index ab13c4565974..110058a2591c 100644 --- a/src/Http/Http.Results/src/Ok.cs +++ b/src/Http/Http.Results/src/Ok.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -43,10 +45,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK)); } } diff --git a/src/Http/Http.Results/src/OkOfT.cs b/src/Http/Http.Results/src/OkOfT.cs index 532837bea11e..d5a9646f70f4 100644 --- a/src/Http/Http.Results/src/OkOfT.cs +++ b/src/Http/Http.Results/src/OkOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -57,10 +59,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status200OK, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status200OK, "application/json")); } } diff --git a/src/Http/Http.Results/src/ResultsOfT.Generated.cs b/src/Http/Http.Results/src/ResultsOfT.Generated.cs index c468a4e7541e..6034e656b573 100644 --- a/src/Http/Http.Results/src/ResultsOfT.Generated.cs +++ b/src/Http/Http.Results/src/ResultsOfT.Generated.cs @@ -3,6 +3,8 @@ // This file is generated by a tool. See: src/Http/Http.Results/tools/ResultsOfTGenerator +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; namespace Microsoft.AspNetCore.Http.HttpResults; @@ -59,12 +61,13 @@ public Task ExecuteAsync(HttpContext httpContext) public static implicit operator Results(TResult2 result) => new(result); /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); } } @@ -128,13 +131,14 @@ public Task ExecuteAsync(HttpContext httpContext) public static implicit operator Results(TResult3 result) => new(result); /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); } } @@ -206,14 +210,15 @@ public Task ExecuteAsync(HttpContext httpContext) public static implicit operator Results(TResult4 result) => new(result); /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); } } @@ -293,15 +298,16 @@ public Task ExecuteAsync(HttpContext httpContext) public static implicit operator Results(TResult5 result) => new(result); /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); - - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); + + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); } } @@ -389,15 +395,16 @@ public Task ExecuteAsync(HttpContext httpContext) public static implicit operator Results(TResult6 result) => new(result); /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); - - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); - ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); + + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); + ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder); } } diff --git a/src/Http/Http.Results/src/ResultsOfTHelper.cs b/src/Http/Http.Results/src/ResultsOfTHelper.cs index 7f6361167158..a68f55c6dfee 100644 --- a/src/Http/Http.Results/src/ResultsOfTHelper.cs +++ b/src/Http/Http.Results/src/ResultsOfTHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; namespace Microsoft.AspNetCore.Http; @@ -10,16 +11,16 @@ internal static class ResultsOfTHelper { private static readonly MethodInfo PopulateMetadataMethod = typeof(ResultsOfTHelper).GetMethod(nameof(PopulateMetadata), BindingFlags.Static | BindingFlags.NonPublic)!; - public static void PopulateMetadataIfTargetIsIEndpointMetadataProvider(EndpointMetadataContext context) + public static void PopulateMetadataIfTargetIsIEndpointMetadataProvider(MethodInfo method, EndpointBuilder builder) { if (typeof(IEndpointMetadataProvider).IsAssignableFrom(typeof(TTarget))) { - PopulateMetadataMethod.MakeGenericMethod(typeof(TTarget)).Invoke(null, new object[] { context }); + PopulateMetadataMethod.MakeGenericMethod(typeof(TTarget)).Invoke(null, new object[] { method, builder }); } } - private static void PopulateMetadata(EndpointMetadataContext context) where TTarget : IEndpointMetadataProvider + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where TTarget : IEndpointMetadataProvider { - TTarget.PopulateMetadata(context); + TTarget.PopulateMetadata(method, builder); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntity.cs b/src/Http/Http.Results/src/UnprocessableEntity.cs index 6a56964d17ba..cc38731f1165 100644 --- a/src/Http/Http.Results/src/UnprocessableEntity.cs +++ b/src/Http/Http.Results/src/UnprocessableEntity.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,10 +46,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity)); + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity)); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs index b1abbf6faece..d4eca8216826 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs +++ b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -58,10 +60,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status422UnprocessableEntity, "application/json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status422UnprocessableEntity, "application/json")); } } diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs index 36631d141e8b..84ead95982d5 100644 --- a/src/Http/Http.Results/src/ValidationProblem.cs +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -65,10 +67,11 @@ public Task ExecuteAsync(HttpContext httpContext) } /// - static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context) + static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(method); + ArgumentNullException.ThrowIfNull(builder); - context.EndpointMetadata.Add(new ProducesResponseTypeMetadata(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")); + builder.Metadata.Add(new ProducesResponseTypeMetadata(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")); } } diff --git a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs index 40add2624bca..a4b085a27f9f 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs @@ -3,8 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; @@ -121,14 +123,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() { // Arrange AcceptedAtRoute MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -146,10 +147,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -201,8 +203,8 @@ public void AcceptedAtRouteResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs index 8f00d120d7f4..cf11ba325d86 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs @@ -3,8 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; @@ -75,13 +77,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange AcceptedAtRoute MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); } @@ -97,10 +99,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -111,8 +114,8 @@ public void AcceptedAtRouteResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static HttpContext GetHttpContext(LinkGenerator linkGenerator) { diff --git a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs index 9f84c90d3b96..669eba8d529c 100644 --- a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs @@ -3,8 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http.HttpResults; @@ -63,13 +65,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Accepted MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -87,10 +89,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -125,8 +128,8 @@ public void AcceptedResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/AcceptedResultTests.cs b/src/Http/Http.Results/test/AcceptedResultTests.cs index d7233b14cfce..0e17089db4e1 100644 --- a/src/Http/Http.Results/test/AcceptedResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedResultTests.cs @@ -3,7 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http.HttpResults; @@ -32,13 +35,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Accepted MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ProducesResponseTypeMetadata { StatusCode: StatusCodes.Status202Accepted }); + Assert.Contains(builder.Metadata, m => m is ProducesResponseTypeMetadata { StatusCode: StatusCodes.Status202Accepted }); } [Fact] @@ -53,10 +56,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -67,8 +71,8 @@ public void AcceptedResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static HttpContext GetHttpContext() { diff --git a/src/Http/Http.Results/test/BadRequestOfTResultTests.cs b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs index da4e22dbd314..2123a5c4b755 100644 --- a/src/Http/Http.Results/test/BadRequestOfTResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs @@ -5,8 +5,11 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -105,13 +108,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange BadRequest MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -129,10 +132,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -167,8 +171,8 @@ public void BadRequestObjectResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/BadRequestResultTests.cs b/src/Http/Http.Results/test/BadRequestResultTests.cs index 90855243c716..3555def21b4c 100644 --- a/src/Http/Http.Results/test/BadRequestResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestResultTests.cs @@ -5,7 +5,10 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -45,13 +48,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange BadRequest MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); } @@ -67,10 +70,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -81,8 +85,8 @@ public void BadRequestObjectResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceProvider CreateServices() { diff --git a/src/Http/Http.Results/test/ConflictOfTResultTests.cs b/src/Http/Http.Results/test/ConflictOfTResultTests.cs index 467460f7579e..5a249b05ad08 100644 --- a/src/Http/Http.Results/test/ConflictOfTResultTests.cs +++ b/src/Http/Http.Results/test/ConflictOfTResultTests.cs @@ -5,8 +5,11 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -83,13 +86,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Conflict MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status409Conflict, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -107,10 +110,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -145,8 +149,8 @@ public void ConflictObjectResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/ConflictResultTests.cs b/src/Http/Http.Results/test/ConflictResultTests.cs index 1ee5f5d280db..c6919e9ac620 100644 --- a/src/Http/Http.Results/test/ConflictResultTests.cs +++ b/src/Http/Http.Results/test/ConflictResultTests.cs @@ -5,8 +5,11 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -46,13 +49,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Conflict MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status409Conflict, producesResponseTypeMetadata.StatusCode); } @@ -68,10 +71,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -82,8 +86,8 @@ public void ConflictObjectResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceProvider CreateServices() { diff --git a/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs index 7135db889fff..709e227699ee 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -89,13 +91,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange CreatedAtRoute MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -113,10 +115,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -165,8 +168,8 @@ public void CreatedAtRouteResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs index 2350aa400b2b..cfb42575e89c 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -71,13 +73,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange CreatedAtRoute MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); } @@ -93,10 +95,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -112,8 +115,8 @@ public void CreatedAtRouteResult_Implements_IValueHttpResult_Correctly() Assert.Equal(StatusCodes.Status201Created, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static HttpContext GetHttpContext(string expectedUrl) { diff --git a/src/Http/Http.Results/test/CreatedOfTResultTests.cs b/src/Http/Http.Results/test/CreatedOfTResultTests.cs index 007cf5e86c0c..b5544b0c13bf 100644 --- a/src/Http/Http.Results/test/CreatedOfTResultTests.cs +++ b/src/Http/Http.Results/test/CreatedOfTResultTests.cs @@ -3,7 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -97,13 +100,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Created MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -121,10 +124,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -164,8 +168,8 @@ public void AcceptedResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/CreatedResultTests.cs b/src/Http/Http.Results/test/CreatedResultTests.cs index 493c5885744b..4a62a238b5c4 100644 --- a/src/Http/Http.Results/test/CreatedResultTests.cs +++ b/src/Http/Http.Results/test/CreatedResultTests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -63,13 +66,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Created MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); } @@ -85,10 +88,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -102,8 +106,8 @@ public void CreatedResult_Implements_IValueHttpResult_Correctly() Assert.Equal(StatusCodes.Status201Created, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static HttpContext GetHttpContext() { diff --git a/src/Http/Http.Results/test/NoContentResultTests.cs b/src/Http/Http.Results/test/NoContentResultTests.cs index f2fe39be7b70..be210eae40b1 100644 --- a/src/Http/Http.Results/test/NoContentResultTests.cs +++ b/src/Http/Http.Results/test/NoContentResultTests.cs @@ -4,7 +4,10 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -42,13 +45,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange NoContent MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status204NoContent, producesResponseTypeMetadata.StatusCode); } @@ -64,10 +67,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -78,8 +82,8 @@ public void NoContentResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceCollection CreateServices() { diff --git a/src/Http/Http.Results/test/NotFoundOfTResultTests.cs b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs index 73a970ea1c98..029dd8f79815 100644 --- a/src/Http/Http.Results/test/NotFoundOfTResultTests.cs +++ b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -65,13 +68,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange NotFound MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status404NotFound, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -89,10 +92,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -127,8 +131,8 @@ public void NotFoundResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/NotFoundResultTests.cs b/src/Http/Http.Results/test/NotFoundResultTests.cs index 7568b5fb9c5a..d6c3c9047901 100644 --- a/src/Http/Http.Results/test/NotFoundResultTests.cs +++ b/src/Http/Http.Results/test/NotFoundResultTests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -41,13 +44,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange NotFound MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status404NotFound, producesResponseTypeMetadata.StatusCode); } @@ -63,10 +66,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -77,8 +81,8 @@ public void NotFoundResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static HttpContext GetHttpContext() { diff --git a/src/Http/Http.Results/test/OkOfTResultTests.cs b/src/Http/Http.Results/test/OkOfTResultTests.cs index a4274ce24194..8accca9174d6 100644 --- a/src/Http/Http.Results/test/OkOfTResultTests.cs +++ b/src/Http/Http.Results/test/OkOfTResultTests.cs @@ -3,7 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -82,13 +85,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Ok MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -106,10 +109,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -144,8 +148,8 @@ public void OkResult_Implements_IValueHttpResultOfT_Correctly() Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/OkResultTests.cs b/src/Http/Http.Results/test/OkResultTests.cs index 4d3add27c9ec..f8c341e441e9 100644 --- a/src/Http/Http.Results/test/OkResultTests.cs +++ b/src/Http/Http.Results/test/OkResultTests.cs @@ -3,7 +3,10 @@ using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -45,13 +48,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange Ok MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); } @@ -67,10 +70,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -81,8 +85,8 @@ public void OkObjectResult_Implements_IStatusCodeHttpResult_Correctly() Assert.Equal(StatusCodes.Status200OK, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceProvider CreateServices() { diff --git a/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs b/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs index 618fe5fed1c6..9ad1e753eefa 100644 --- a/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs +++ b/src/Http/Http.Results/test/ResultsOfTTests.Generated.cs @@ -3,12 +3,11 @@ // This file is generated by a tool. See: src/Http/Http.Results/tools/ResultsOfTGenerator using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Http.Result; @@ -207,22 +206,22 @@ public void ResultsOfTResult1TResult2_PopulateMetadata_PopulatesMetadataFromType { // Arrange Results MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); } [Fact] - public void ResultsOfTResult1TResult2_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + public void ResultsOfTResult1TResult2_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null)); } [Theory] @@ -486,23 +485,23 @@ public void ResultsOfTResult1TResult2TResult3_PopulateMetadata_PopulatesMetadata { // Arrange Results MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); } [Fact] - public void ResultsOfTResult1TResult2TResult3_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + public void ResultsOfTResult1TResult2TResult3_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null)); } [Theory] @@ -842,24 +841,24 @@ public void ResultsOfTResult1TResult2TResult3TResult4_PopulateMetadata_Populates { // Arrange Results MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); } [Fact] - public void ResultsOfTResult1TResult2TResult3TResult4_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + public void ResultsOfTResult1TResult2TResult3TResult4_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null)); } [Theory] @@ -1283,25 +1282,25 @@ public void ResultsOfTResult1TResult2TResult3TResult4TResult5_PopulateMetadata_P { // Arrange Results MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult5) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult5) }); } [Fact] - public void ResultsOfTResult1TResult2TResult3TResult4TResult5_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + public void ResultsOfTResult1TResult2TResult3TResult4TResult5_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null)); } [Theory] @@ -1817,26 +1816,26 @@ public void ResultsOfTResult1TResult2TResult3TResult4TResult5TResult6_PopulateMe { // Arrange Results MyApi() { throw new NotImplementedException(); } - var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult5) }); - Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult6) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult3) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult4) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult5) }); + Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult6) }); } [Fact] - public void ResultsOfTResult1TResult2TResult3TResult4TResult5TResult6_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + public void ResultsOfTResult1TResult2TResult3TResult4TResult5TResult6_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null)); } abstract class ChecksumResult : IResult @@ -1863,9 +1862,9 @@ class ProvidesMetadataResult1 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult1) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult1) }); } } @@ -1877,9 +1876,9 @@ class ProvidesMetadataResult2 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult2) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult2) }); } } @@ -1891,9 +1890,9 @@ class ProvidesMetadataResult3 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult3) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult3) }); } } @@ -1905,9 +1904,9 @@ class ProvidesMetadataResult4 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult4) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult4) }); } } @@ -1919,9 +1918,9 @@ class ProvidesMetadataResult5 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult5) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult5) }); } } @@ -1933,9 +1932,9 @@ class ProvidesMetadataResult6 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult6) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult6) }); } } class ChecksumResult7 : ChecksumResult @@ -1946,9 +1945,9 @@ class ProvidesMetadataResult7 : IResult, IEndpointMetadataProvider { public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult7) }); + builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult7) }); } } diff --git a/src/Http/Http.Results/test/ResultsOfTTests.cs b/src/Http/Http.Results/test/ResultsOfTTests.cs index b68cd6eca904..c86f06da886e 100644 --- a/src/Http/Http.Results/test/ResultsOfTTests.cs +++ b/src/Http/Http.Results/test/ResultsOfTTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -85,9 +87,9 @@ private void AssertFileContentEqual(string expected, string actual, string type) } } - private static void PopulateMetadata(EndpointMetadataContext context) where TTarget : IEndpointMetadataProvider + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where TTarget : IEndpointMetadataProvider { - TTarget.PopulateMetadata(context); + TTarget.PopulateMetadata(method, builder); } private class ResultTypeProvidedMetadata diff --git a/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs index 4d8190b70bb9..61d0191958ee 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs @@ -5,7 +5,10 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -82,13 +85,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange UnprocessableEntity MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata>(context); + PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status422UnprocessableEntity, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); @@ -106,10 +109,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata>(null)); + Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata>(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -144,8 +148,8 @@ public void UnprocessableEntityObjectResult_Implements_IValueHttpResultOfT_Corre Assert.Equal(value, result.Value); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private record Todo(int Id, string Title); diff --git a/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs index d5f97654477c..d49b172da003 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs @@ -5,7 +5,10 @@ namespace Microsoft.AspNetCore.Http.HttpResults; using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -45,13 +48,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange UnprocessableEntity MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status422UnprocessableEntity, producesResponseTypeMetadata.StatusCode); } @@ -67,10 +70,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -81,8 +85,8 @@ public void UnprocessableEntityObjectResult_Implements_IStatusCodeHttpResult_Cor Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceProvider CreateServices() { diff --git a/src/Http/Http.Results/test/ValidationProblemResultTests.cs b/src/Http/Http.Results/test/ValidationProblemResultTests.cs index 55e672bb311e..b846cc9d4d2c 100644 --- a/src/Http/Http.Results/test/ValidationProblemResultTests.cs +++ b/src/Http/Http.Results/test/ValidationProblemResultTests.cs @@ -3,9 +3,12 @@ using System.Reflection; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -63,13 +66,13 @@ public void PopulateMetadata_AddsResponseTypeMetadata() // Arrange ValidationProblem MyApi() { throw new NotImplementedException(); } var metadata = new List(); - var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act - PopulateMetadata(context); + PopulateMetadata(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - var producesResponseTypeMetadata = context.EndpointMetadata.OfType().Last(); + var producesResponseTypeMetadata = builder.Metadata.OfType().Last(); Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(HttpValidationProblemDetails), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/problem+json"); @@ -87,10 +90,11 @@ public void ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() } [Fact] - public void PopulateMetadata_ThrowsArgumentNullException_WhenContextIsNull() + public void PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull() { // Act & Assert - Assert.Throws("context", () => PopulateMetadata(null)); + Assert.Throws("method", () => PopulateMetadata(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + Assert.Throws("builder", () => PopulateMetadata(((Delegate)PopulateMetadata_ThrowsArgumentNullException_WhenMethodOrBuilderAreNull).GetMethodInfo(), null)); } [Fact] @@ -136,8 +140,8 @@ public void ValidationProblemResult_Implements_IContentTypeHttpResult_Correctly( Assert.Equal(contentType, result.ContentType); } - private static void PopulateMetadata(EndpointMetadataContext context) - where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(context); + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static IServiceProvider CreateServices() { diff --git a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs index c23363e06e86..6dd187576438 100644 --- a/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs +++ b/src/Http/Http.Results/tools/ResultsOfTGenerator/Program.cs @@ -60,6 +60,8 @@ static void GenerateClassFile(string classFilePath, int typeArgCount, bool inter writer.WriteLine(); // Usings + writer.WriteLine("using System.Reflection;"); + writer.WriteLine("using Microsoft.AspNetCore.Builder;"); writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;"); writer.WriteLine(); @@ -177,13 +179,14 @@ static void GenerateClassFile(string classFilePath, int typeArgCount, bool inter // IEndpointMetadataProvider.PopulateMetadata writer.WriteIndentedLine("/// "); - writer.WriteIndentedLine("static void IEndpointMetadataProvider.PopulateMetadata(EndpointMetadataContext context)"); + writer.WriteIndentedLine("static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)"); writer.WriteIndentedLine("{"); - writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(context);"); + writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(method);"); + writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(builder);"); writer.WriteLine(); for (int j = 1; j <= i; j++) { - writer.WriteIndentedLine(2, $"ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(context);"); + writer.WriteIndentedLine(2, $"ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider(method, builder);"); } writer.WriteIndentedLine("}"); @@ -231,12 +234,11 @@ static void GenerateTestFiles(string testFilePath, int typeArgCount, bool intera // Using statements writer.WriteLine("using System.Reflection;"); - writer.WriteLine("using System.Threading.Tasks;"); - writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;"); + writer.WriteLine("using Microsoft.AspNetCore.Builder;"); writer.WriteLine("using Microsoft.AspNetCore.Http.HttpResults;"); - writer.WriteLine("using Microsoft.Extensions.DependencyInjection;"); - writer.WriteLine("using Microsoft.Extensions.Logging;"); - writer.WriteLine("using Microsoft.Extensions.Logging.Abstractions;"); + writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;"); + writer.WriteLine("using Microsoft.AspNetCore.Routing;"); + writer.WriteLine("using Microsoft.AspNetCore.Routing.Patterns;"); writer.WriteLine(); // Namespace @@ -262,7 +264,7 @@ static void GenerateTestFiles(string testFilePath, int typeArgCount, bool intera GenerateTest_AcceptsIResult_AsAnyTypeArg(writer, i); GenerateTest_AcceptsNestedResultsOfT_AsAnyTypeArg(writer, i); GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImplementIEndpointMetadataProvider(writer, i); - GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull(writer, i); + GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderIsNull(writer, i); } Generate_ChecksumResultClass(writer); @@ -813,15 +815,14 @@ static void GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImple //{ // // Arrange // Results MyApi() { throw new NotImplementedException(); } - // var metadata = new List(); - // var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); + // var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0); // // Act - // PopulateMetadata>(context); + // PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // // Assert - // Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); - // Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); + // Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) }); + // Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) }); //} // Attributes @@ -849,8 +850,7 @@ static void GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImple } } writer.WriteLine("> MyApi() { throw new NotImplementedException(); }"); - writer.WriteIndentedLine(2, "var metadata = new List();"); - writer.WriteIndentedLine(2, "var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance);"); + writer.WriteIndentedLine(2, @"var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0);"); writer.WriteLine(); // Act @@ -865,14 +865,14 @@ static void GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImple writer.Write(", "); } } - writer.WriteLine(">>(context);"); + writer.WriteLine(">>(((Delegate)MyApi).GetMethodInfo(), builder);"); writer.WriteLine(); // Assert writer.WriteIndentedLine(2, "// Assert"); for (int j = 1; j <= typeArgNumber; j++) { - writer.WriteIndentedLine(2, $"Assert.Contains(context.EndpointMetadata, m => m is ResultTypeProvidedMetadata {{ SourceTypeName: nameof(ProvidesMetadataResult{j}) }});"); + writer.WriteIndentedLine(2, $"Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata {{ SourceTypeName: nameof(ProvidesMetadataResult{j}) }});"); } // Close method @@ -880,13 +880,14 @@ static void GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImple writer.WriteLine(); } - static void GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull(StreamWriter writer, int typeArgNumber) + static void GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderIsNull(StreamWriter writer, int typeArgNumber) { //[Fact] - //public void ResultsOfTResult1TResult2_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull() + //public void ResultsOfTResult1TResult2_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull() //{ // // Act & Assert - // Assert.Throws("context", () => PopulateMetadata>(null)); + // Assert.Throws("method", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0))); + // Assert.Throws("builder", () => PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), null)); //} // Attributes @@ -898,12 +899,24 @@ static void GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenConte { writer.Write($"TResult{j}"); } - writer.WriteLine("_PopulateMetadata_Throws_ArgumentNullException_WhenContextIsNull()"); + writer.WriteLine("_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull()"); writer.WriteIndentedLine("{"); // Act & Assert writer.WriteIndentedLine(2, "// Act & Assert"); - writer.WriteIndent(2, "Assert.Throws(\"context\", () => PopulateMetadata(\"method\", () => PopulateMetadata>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0)));"); + + writer.WriteIndent(2, "Assert.Throws(\"builder\", () => PopulateMetadata>(null));"); + writer.WriteLine(">>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null));"); // Close method writer.WriteIndentedLine(1, "}"); @@ -960,18 +973,18 @@ static void Generate_ProvidesMetadataResultClass(StreamWriter writer, int typeAr //{ // public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask; - // public static void PopulateMetadata(EndpointMetadataContext context) + // public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) // { - // context.EndpointMetadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult1) }); + // builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult1) }); // } //} writer.WriteIndentedLine(1, $"class ProvidesMetadataResult{typeArgNumber} : IResult, IEndpointMetadataProvider"); writer.WriteIndentedLine(1, "{"); writer.WriteIndentedLine(2, "public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask;"); writer.WriteLine(); - writer.WriteIndentedLine(2, "public static void PopulateMetadata(EndpointMetadataContext context)"); + writer.WriteIndentedLine(2, "public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)"); writer.WriteIndentedLine(2, "{"); - writer.WriteIndentedLine(3, $"context.EndpointMetadata.Add(new ResultTypeProvidedMetadata {{ SourceTypeName = nameof(ProvidesMetadataResult{typeArgNumber}) }});"); + writer.WriteIndentedLine(3, $"builder.Metadata.Add(new ResultTypeProvidedMetadata {{ SourceTypeName = nameof(ProvidesMetadataResult{typeArgNumber}) }});"); writer.WriteIndentedLine(2, "}"); writer.WriteIndentedLine(1, "}"); } diff --git a/src/Http/Routing/src/EndpointDataSource.cs b/src/Http/Routing/src/EndpointDataSource.cs index 9d5e05865428..c6a2fd7944f7 100644 --- a/src/Http/Routing/src/EndpointDataSource.cs +++ b/src/Http/Routing/src/EndpointDataSource.cs @@ -52,9 +52,7 @@ public virtual IReadOnlyList GetGroupedEndpoints(RouteGroupContext con // Make the full route pattern visible to IEndpointConventionBuilder extension methods called on the group. // This includes patterns from any parent groups. var fullRoutePattern = RoutePatternFactory.Combine(context.Prefix, routeEndpoint.RoutePattern); - - // RequestDelegate can never be null on a RouteEndpoint. The nullability carries over from Endpoint. - var routeEndpointBuilder = new RouteEndpointBuilder(routeEndpoint.RequestDelegate!, fullRoutePattern, routeEndpoint.Order) + var routeEndpointBuilder = new RouteEndpointBuilder(routeEndpoint.RequestDelegate, fullRoutePattern, routeEndpoint.Order) { DisplayName = routeEndpoint.DisplayName, ApplicationServices = context.ApplicationServices, diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index a3b111566e58..6347092be335 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -1,9 +1,11 @@ #nullable enable +*REMOVED*Microsoft.AspNetCore.Routing.RouteEndpointBuilder.RouteEndpointBuilder(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, Microsoft.AspNetCore.Routing.Patterns.RoutePattern! routePattern, int order) -> void Microsoft.AspNetCore.Builder.RouteHandlerBuilder.Finally(System.Action! finalConvention) -> void Microsoft.AspNetCore.Http.EndpointFilterExtensions Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.Dispose() -> void Microsoft.AspNetCore.Routing.HttpMethodMetadata.AcceptCorsPreflight.set -> void Microsoft.AspNetCore.Routing.IHttpMethodMetadata.AcceptCorsPreflight.set -> void +Microsoft.AspNetCore.Routing.RouteEndpointBuilder.RouteEndpointBuilder(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Routing.Patterns.RoutePattern! routePattern, int order) -> void Microsoft.AspNetCore.Routing.RouteGroupBuilder Microsoft.AspNetCore.Routing.RouteGroupContext Microsoft.AspNetCore.Routing.RouteGroupContext.ApplicationServices.get -> System.IServiceProvider! diff --git a/src/Http/Routing/src/RouteEndpointBuilder.cs b/src/Http/Routing/src/RouteEndpointBuilder.cs index 2498426d06e7..8c37f1047991 100644 --- a/src/Http/Routing/src/RouteEndpointBuilder.cs +++ b/src/Http/Routing/src/RouteEndpointBuilder.cs @@ -30,10 +30,12 @@ public sealed class RouteEndpointBuilder : EndpointBuilder /// The to use in URL matching. /// The order assigned to the endpoint. public RouteEndpointBuilder( - RequestDelegate requestDelegate, + RequestDelegate? requestDelegate, RoutePattern routePattern, int order) { + ArgumentNullException.ThrowIfNull(routePattern); + RequestDelegate = requestDelegate; RoutePattern = routePattern; Order = order; diff --git a/src/Http/Routing/src/RouteEndpointDataSource.cs b/src/Http/Routing/src/RouteEndpointDataSource.cs index e6c3599c3107..7e38da886cb5 100644 --- a/src/Http/Routing/src/RouteEndpointDataSource.cs +++ b/src/Http/Routing/src/RouteEndpointDataSource.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -184,6 +185,19 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( } } + RequestDelegateFactoryOptions? rdfOptions = null; + RequestDelegateMetadataResult? rdfMetadataResult = null; + + // Any metadata inferred directly inferred by RDF or indirectly inferred via IEndpoint(Parameter)MetadataProviders are + // considered less specific than method-level attributes and conventions but more specific than group conventions + // so inferred metadata gets added in between these. If group conventions need to override inferred metadata, + // they can do so via IEndpointConventionBuilder.Finally like the do to override any other entry-specific metadata. + if (isRouteHandler) + { + rdfOptions = CreateRDFOptions(entry, pattern, builder); + rdfMetadataResult = RequestDelegateFactory.InferMetadata(entry.RouteHandler.Method, rdfOptions); + } + // Add delegate attributes as metadata before entry-specific conventions but after group conventions. var attributes = handler.Method.GetCustomAttributes(); if (attributes is not null) @@ -200,34 +214,23 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( entrySpecificConvention(builder); } + // If no convention has modified builder.RequestDelegate, we can use the RequestDelegate returned by the RequestDelegateFactory directly. + var conventionOverriddenRequestDelegate = ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate) ? null : builder.RequestDelegate; + if (isRouteHandler || builder.FilterFactories.Count > 0) { - var routeParamNames = new List(pattern.Parameters.Count); - foreach (var parameter in pattern.Parameters) - { - routeParamNames.Add(parameter.Name); - } - - RequestDelegateFactoryOptions factoryOptions = new() - { - ServiceProvider = _applicationServices, - RouteParameterNames = routeParamNames, - ThrowOnBadRequest = _throwOnBadRequest, - DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods), - EndpointMetadata = builder.Metadata, - EndpointFilterFactories = builder.FilterFactories.AsReadOnly(), - }; + rdfOptions ??= CreateRDFOptions(entry, pattern, builder); // We ignore the returned EndpointMetadata has been already populated since we passed in non-null EndpointMetadata. - factoryCreatedRequestDelegate = RequestDelegateFactory.Create(entry.RouteHandler, factoryOptions).RequestDelegate; + // We always set factoryRequestDelegate in case something is still referencing the redirected version of the RequestDelegate. + factoryCreatedRequestDelegate = RequestDelegateFactory.Create(entry.RouteHandler, rdfOptions, rdfMetadataResult).RequestDelegate; } - if (ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate)) - { - // No convention has changed builder.RequestDelegate, so we can just replace it with the final version as an optimization. - // We still set factoryRequestDelegate in case something is still referencing the redirected version of the RequestDelegate. - builder.RequestDelegate = factoryCreatedRequestDelegate; - } + Debug.Assert(factoryCreatedRequestDelegate is not null); + + // Use the overridden RequestDelegate if it exists. If the overridden RequestDelegate is merely wrapping the final RequestDelegate, + // it will still work because of the redirectRequestDelegate. + builder.RequestDelegate = conventionOverriddenRequestDelegate ?? factoryCreatedRequestDelegate; entry.FinallyConventions.IsReadOnly = true; foreach (var entryFinallyConvention in entry.FinallyConventions) @@ -248,6 +251,24 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( return builder; } + private RequestDelegateFactoryOptions CreateRDFOptions(RouteEntry entry, RoutePattern pattern, RouteEndpointBuilder builder) + { + var routeParamNames = new List(pattern.Parameters.Count); + foreach (var parameter in pattern.Parameters) + { + routeParamNames.Add(parameter.Name); + } + + return new() + { + ServiceProvider = _applicationServices, + RouteParameterNames = routeParamNames, + ThrowOnBadRequest = _throwOnBadRequest, + DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods), + EndpointBuilder = builder, + }; + } + private static bool ShouldDisableInferredBodyParameters(IEnumerable? httpMethods) { static bool ShouldDisableInferredBodyForMethod(string method) => diff --git a/src/Http/Routing/test/UnitTests/Builder/GroupTest.cs b/src/Http/Routing/test/UnitTests/Builder/GroupTest.cs index 53ae8cae1ce8..39c0539f3b4d 100644 --- a/src/Http/Routing/test/UnitTests/Builder/GroupTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/GroupTest.cs @@ -259,21 +259,24 @@ public async Task ChangingMostEndpointBuilderPropertiesInConvention_Works() var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(EmptyServiceProvider.Instance)); var group = builder.MapGroup("/group"); - var mapGetCalled = false; + var mapGetCallCount = 0; var replacementCalled = false; group.MapGet("/", () => { - mapGetCalled = true; + mapGetCallCount++; }); ((IEndpointConventionBuilder)group).Add(builder => { + var originalRequestDelegate = builder.RequestDelegate!; + builder.DisplayName = $"Prefixed! {builder.DisplayName}"; - builder.RequestDelegate = ctx => + builder.RequestDelegate = async ctx => { replacementCalled = true; - return Task.CompletedTask; + await originalRequestDelegate(ctx); + await originalRequestDelegate(ctx); }; ((RouteEndpointBuilder)builder).Order = 42; @@ -286,8 +289,8 @@ public async Task ChangingMostEndpointBuilderPropertiesInConvention_Works() await endpoint!.RequestDelegate!(httpContext); - Assert.False(mapGetCalled); Assert.True(replacementCalled); + Assert.Equal(2, mapGetCallCount); Assert.Equal("Prefixed! HTTP: GET /group/", endpoint.DisplayName); var routeEndpoint = Assert.IsType(endpoint); diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index ee89fa40885d..c58a14ed4223 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -115,7 +115,6 @@ public async Task MapEndpoint_CanBeFiltered_ByEndpointFilters(Func { - routeHandlerContext.EndpointMetadata.Add(filterTag); return async invocationContext => { Assert.IsAssignableFrom(Assert.Single(invocationContext.Arguments)); @@ -129,7 +128,6 @@ public async Task MapEndpoint_CanBeFiltered_ByEndpointFilters(Func()); Assert.NotNull(endpoint.RequestDelegate); var requestDelegate = endpoint.RequestDelegate!; @@ -147,11 +145,11 @@ public void MapEndpoint_UsesOriginalRequestDelegateInstance_IfFilterDoesNotChang var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(EmptyServiceProvider.Instance)); RequestDelegate initialRequestDelegate = static (context) => Task.CompletedTask; - var filterTag = new TagsAttribute("filter"); + var runCount = 0; var endpointBuilder = map(builder, "/", initialRequestDelegate).AddEndpointFilterFactory((routeHandlerContext, next) => { - routeHandlerContext.EndpointMetadata.Add(filterTag); + runCount++; return next; }); @@ -159,7 +157,7 @@ public void MapEndpoint_UsesOriginalRequestDelegateInstance_IfFilterDoesNotChang var endpoint = Assert.Single(dataSource.Endpoints); Assert.Same(initialRequestDelegate, endpoint.RequestDelegate); - Assert.Same(filterTag, endpoint.Metadata.GetMetadata()); + Assert.Equal(1, runCount); } [Fact] @@ -306,9 +304,6 @@ public void Map_AddsMetadata_InCorrectOrder() Assert.Collection(metadata, m => Assert.IsAssignableFrom(m), - m => Assert.Equal("System.Runtime.CompilerServices.NullableContextAttribute", m.ToString()), - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), m => Assert.IsAssignableFrom(m), m => { @@ -319,7 +314,10 @@ public void Map_AddsMetadata_InCorrectOrder() { Assert.IsAssignableFrom(m); Assert.Equal(MetadataSource.ReturnType, ((CustomEndpointMetadata)m).Source); - }); + }, + m => Assert.Equal("System.Runtime.CompilerServices.NullableContextAttribute", m.ToString()), + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m)); } [Attribute1] @@ -351,9 +349,9 @@ private class Attribute2 : Attribute private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IResult { - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); @@ -361,16 +359,16 @@ public static void PopulateMetadata(EndpointMetadataContext context) private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) { - parameterContext.EndpointMetadata.Add(new ParameterNameMetadata { Name = parameterContext.Parameter.Name ?? string.Empty }); + builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name ?? string.Empty }); } - public static void PopulateMetadata(EndpointMetadataContext context) + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } } diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index ad2f1dc3d7e6..0b496b396c30 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -1007,26 +1008,6 @@ public void RequestDelegateFactory_ProvidesAppServiceProvider_ToFilterFactory() Assert.True(filterFactoryRan); } - [Fact] - public void RouteHandlerContext_ThrowsArgumentNullException_ForMethodInfo() - { - Assert.Throws("methodInfo", () => new EndpointFilterFactoryContext(null!, new List(), new ServiceCollection().BuildServiceProvider())); - } - - [Fact] - public void RouteHandlerContext_ThrowsArgumentNullException_ForEndpointMetadata() - { - var handler = () => { }; - Assert.Throws("endpointMetadata", () => new EndpointFilterFactoryContext(handler.Method, null!, new ServiceCollection().BuildServiceProvider())); - } - - [Fact] - public void RouteHandlerContext_ThrowsArgumentNullException_ForApplicationServices() - { - var handler = () => { }; - Assert.Throws("applicationServices", () => new EndpointFilterFactoryContext(handler.Method, new List(), null!)); - } - [Fact] public void FinallyOnGroup_CanExamineFinallyOnEndpoint() { diff --git a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs index 7539757125c1..669962719644 100644 --- a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs @@ -9,6 +9,20 @@ namespace Microsoft.AspNetCore.Routing; public class RouteEndpointBuilderTest { + [Fact] + public void Constructor_AllowsNullRequestDelegate() + { + var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); + Assert.Null(builder.RequestDelegate); + } + + [Fact] + public void Constructor_DoesNotAllowNullRoutePattern() + { + var ex = Assert.Throws(() => new RouteEndpointBuilder(context => Task.CompletedTask, routePattern: null, order: 0)); + Assert.Equal("routePattern", ex.ParamName); + } + [Fact] public void Build_AllValuesSet_EndpointCreated() { diff --git a/src/Http/samples/MinimalSample/MinimalSample.csproj b/src/Http/samples/MinimalSample/MinimalSample.csproj index 4a160b62569e..8c5e9e6306e3 100644 --- a/src/Http/samples/MinimalSample/MinimalSample.csproj +++ b/src/Http/samples/MinimalSample/MinimalSample.csproj @@ -2,6 +2,7 @@ $(DefaultNetCoreTargetFramework) + enable diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index c0705588ae37..c8dd4bbd2c06 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -23,13 +23,15 @@ inner.AddEndpointFilterFactory((routeContext, next) => { - var tags = routeContext.EndpointMetadata.OfType().FirstOrDefault(); + IReadOnlyList? tags = null; return async invocationContext => { + tags ??= invocationContext.HttpContext.GetEndpoint()?.Metadata.GetMetadata()?.Tags ?? Array.Empty(); + Console.WriteLine("Running filter!"); var result = await next(invocationContext); - return ((string)result) + " | /inner filter! Tags:" + tags is null ? "(null)" : string.Join(", ", tags.Tags); + return $"{result} | /inner filter! Tags: {(tags.Count == 0 ? "(null)" : string.Join(", ", tags))}"; }; }); @@ -43,7 +45,7 @@ { Console.WriteLine("Running filter!"); var result = await next(invocationContext); - return ((string)result) + "| nested filter!"; + return $"{result} | nested filter!"; }; }); @@ -62,7 +64,7 @@ string SayHello(string name) => $"Hello, {name}!"; app.MapGet("/hello/{name}", SayHello); -app.MapGet("/null-result", IResult () => null); +app.MapGet("/null-result", IResult () => null!); app.MapGet("/todo/{id}", Results, NotFound, BadRequest> (int id) => id switch { @@ -71,7 +73,7 @@ _ => TypedResults.NotFound() }); -var extensions = new Dictionary() { { "traceId", "traceId123" } }; +var extensions = new Dictionary() { { "traceId", "traceId123" } }; var errors = new Dictionary() { { "Title", new[] { "The Title field is required." } } }; @@ -94,11 +96,11 @@ internal record Todo(int Id, string Title); public class TodoBindable : IBindableFromHttpContext { public int Id { get; set; } - public string Title { get; set; } + public string Title { get; set; } = string.Empty; public bool IsComplete { get; set; } - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - return ValueTask.FromResult(new TodoBindable { Id = 1, Title = "I was bound from IBindableFromHttpContext.BindAsync!" }); + return ValueTask.FromResult(new TodoBindable { Id = 1, Title = "I was bound from IBindableFromHttpContext.BindAsync!" }); } } diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs index 09e0d633f395..d42b6b3f6ed7 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs @@ -23,7 +23,6 @@ public ApiBehaviorApplicationModelProvider( ActionModelConventions = new List() { new ApiVisibilityConvention(), - new EndpointMetadataConvention(serviceProvider) }; if (!options.SuppressMapClientErrors) diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs deleted file mode 100644 index b830993746ab..000000000000 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/EndpointMetadataConvention.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Mvc.ApplicationModels; - -internal sealed class EndpointMetadataConvention : IActionModelConvention -{ - private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataConvention).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; - private readonly IServiceProvider _serviceProvider; - - public EndpointMetadataConvention(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public void Apply(ActionModel action) - { - // Get metadata from parameter types - ApplyParametersMetadata(action); - - // Get metadata from return type - ApplyReturnTypeMetadata(action); - } - - private void ApplyReturnTypeMetadata(ActionModel action) - { - var returnType = action.ActionMethod.ReturnType; - if (AwaitableInfo.IsTypeAwaitable(returnType, out var awaitableInfo)) - { - returnType = awaitableInfo.ResultType; - } - - if (returnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType)) - { - object?[]? invokeArgs = null; - - for (var i = 0; i < action.Selectors.Count; i++) - { - // Return type implements IEndpointMetadataProvider - var context = new EndpointMetadataContext(action.ActionMethod, action.Selectors[i].EndpointMetadata, _serviceProvider); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(returnType).Invoke(null, invokeArgs); - } - } - } - - private void ApplyParametersMetadata(ActionModel action) - { - object?[]? invokeArgs = null; - var parameters = action.ActionMethod.GetParameters(); - - foreach (var parameter in parameters) - { - if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - for (var i = 0; i < action.Selectors.Count; i++) - { - // Parameter type implements IEndpointParameterMetadataProvider - var context = new EndpointParameterMetadataContext(parameter, action.Selectors[i].EndpointMetadata, _serviceProvider); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - } - - if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - for (var i = 0; i < action.Selectors.Count; i++) - { - // Return type implements IEndpointMetadataProvider - var context = new EndpointMetadataContext(action.ActionMethod, action.Selectors[i].EndpointMetadata, _serviceProvider); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - } - } - } - - private static void PopulateMetadataForParameter(EndpointParameterMetadataContext parameterContext) - where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameterContext); - } - - private static void PopulateMetadataForEndpoint(EndpointMetadataContext context) - where T : IEndpointMetadataProvider - { - T.PopulateMetadata(context); - } -} diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 304b7408abaf..7ccc2dcc9a58 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core MVC core components. Contains common action result types, attribute routing, application model conventions, API explorer, application parts, filters, formatters, model binding, and more. @@ -26,6 +26,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs index 8e5fcf61a6da..c16bf8040d76 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs @@ -108,6 +108,7 @@ public void AddEndpoints( var builder = new RouteEndpointBuilder(requestDelegate, updatedRoutePattern, route.Order) { DisplayName = action.DisplayName, + ApplicationServices = _serviceProvider, }; AddActionDataToBuilder( builder, @@ -153,6 +154,7 @@ public void AddEndpoints( var builder = new RouteEndpointBuilder(requestDelegate, updatedRoutePattern, action.AttributeRouteInfo.Order) { DisplayName = action.DisplayName, + ApplicationServices = _serviceProvider, }; AddActionDataToBuilder( builder, @@ -234,6 +236,7 @@ public void AddConventionalLinkGenerationRoute( { new SuppressMatchingMetadata(), }, + ApplicationServices = _serviceProvider, }; if (route.RouteName != null) @@ -244,7 +247,7 @@ public void AddConventionalLinkGenerationRoute( // See comments on the other usage of EndpointNameMetadata in this class. // // The set of cases for a conventional route are much simpler. We don't need to check - // for Endpoint Name already exising here because there's no way to add an attribute to + // for Endpoint Name already existing here because there's no way to add an attribute to // a conventional route. if (route.RouteName != null && routeNames.Add(route.RouteName)) { @@ -336,7 +339,7 @@ private static (RoutePattern resolvedRoutePattern, IDictionary return (attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues); } - private void AddActionDataToBuilder( + private static void AddActionDataToBuilder( EndpointBuilder builder, HashSet routeNames, ActionDescriptor action, @@ -360,7 +363,16 @@ private void AddActionDataToBuilder( groupConventions[i](builder); } - // Add action metadata first so it has a low precedence + var controllerActionDescriptor = action as ControllerActionDescriptor; + + // Add metadata inferred from the parameter and/or return type before action-specific metadata. + // MethodInfo *should* never be null given a ControllerActionDescriptor, but this is unenforced. + if (controllerActionDescriptor?.MethodInfo is not null) + { + EndpointMetadataPopulator.PopulateMetadata(controllerActionDescriptor.MethodInfo, builder); + } + + // Add action-specific metadata early so it has a low precedence if (action.EndpointMetadata != null) { foreach (var d in action.EndpointMetadata) @@ -383,7 +395,7 @@ private void AddActionDataToBuilder( // However, Endpoint Routing requires Endpoint Names to be unique. // // We can use the route name as the endpoint name if it's not set. Note that there's no - // attribute for this today so it's unlikley. Using endpoint name on a + // attribute for this today so it's unlikely. if (routeName != null && !suppressLinkGeneration && routeNames.Add(routeName) && @@ -455,7 +467,7 @@ private void AddActionDataToBuilder( perRouteConventions[i](builder); } - if (builder.FilterFactories.Count > 0 && action is ControllerActionDescriptor cad) + if (builder.FilterFactories.Count > 0 && controllerActionDescriptor is not null) { var routeHandlerFilters = builder.FilterFactories; @@ -466,7 +478,11 @@ private void AddActionDataToBuilder( return controllerInvocationContext.ActionDescriptor.CacheEntry!.InnerActionMethodExecutor.Execute(controllerInvocationContext); }; - var context = new EndpointFilterFactoryContext(cad.MethodInfo, builder.Metadata, _serviceProvider); + var context = new EndpointFilterFactoryContext + { + MethodInfo = controllerActionDescriptor.MethodInfo, + ApplicationServices = builder.ApplicationServices, + }; var initialFilteredInvocation = del; @@ -476,7 +492,7 @@ private void AddActionDataToBuilder( del = filterFactory(context, del); } - cad.FilterDelegate = ReferenceEquals(del, initialFilteredInvocation) ? null : del; + controllerActionDescriptor.FilterDelegate = ReferenceEquals(del, initialFilteredInvocation) ? null : del; } foreach (var perRouteFinallyConvention in perRouteFinallyConventions) diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs index c4dddb5d711a..201fdf8bf153 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs @@ -133,7 +133,6 @@ public void Constructor_SetsUpConventions() Assert.Collection( provider.ActionModelConventions, c => Assert.IsType(c), - c => Assert.IsType(c), c => Assert.IsType(c), c => Assert.IsType(c), c => Assert.IsType(c), diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs deleted file mode 100644 index 37be61a23f0b..000000000000 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataConventionTest.cs +++ /dev/null @@ -1,386 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Moq; - -namespace Microsoft.AspNetCore.Mvc.ApplicationModels; - -public class EndpointMetadataConventionTest -{ - [Theory] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfActionResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInActionResult))] - public void Apply_DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider( - Type controllerType, - string actionName) - { - // Arrange - var action = GetActionModel(controllerType, actionName); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.Contains(action.Selectors[0].EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); - } - - [Fact] - public void Apply_DiscoversEndpointMetadata_ForAllSelectors_FromReturnTypeImplementingIEndpointMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.Contains(selector.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); - } - } - - [Fact] - public void Apply_DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.Contains(action.Selectors[0].EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); - } - - [Fact] - public void Apply_DiscoversEndpointMetadata_ForAllSelectors_FromParametersImplementingIEndpointParameterMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithParameterMetadata)); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.Contains(selector.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); - } - } - - [Fact] - public void Apply_DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.Contains(action.Selectors[0].EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); - } - - [Fact] - public void Apply_DiscoversEndpointMetadata_ForAllSelectors_FromParametersImplementingIEndpointMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithParameterMetadata)); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - foreach (var selector in action.Selectors) - { - Assert.Contains(selector.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); - } - } - - [Fact] - public void Apply_DiscoversMetadata_CorrectOrder() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); - action.Selectors[0].EndpointMetadata.Add(new CustomEndpointMetadata() { Source = MetadataSource.Caller }); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.Collection( - action.Selectors[0].EndpointMetadata, - m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller }), - m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), - m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter })); - } - - [Theory] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInValueTaskOfResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInValueTaskOfActionResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfActionResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInResult))] - [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInActionResult))] - public void Apply_AllowsRemovalOfMetadata_ByReturnTypeImplementingIEndpointMetadataProvider( - Type controllerType, - string actionName) - { - // Arrange - var action = GetActionModel(controllerType, actionName); - action.Selectors[0].EndpointMetadata.Add(new ConsumesAttribute("application/json")); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.DoesNotContain(action.Selectors[0].EndpointMetadata, m => m is IAcceptsMetadata); - } - - [Fact] - public void Apply_AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.ActionWithRemovalFromParameterEndpointMetadata)); - action.Selectors[0].EndpointMetadata.Add(new ConsumesAttribute("application/json")); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.DoesNotContain(action.Selectors[0].EndpointMetadata, m => m is IAcceptsMetadata); - } - - [Fact] - public void Apply_AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointParameterMetadataProvider() - { - // Arrange - var action = GetActionModel(typeof(TestController), nameof(TestController.ActionWithRemovalFromParameterMetadata)); - action.Selectors[0].EndpointMetadata.Add(new ConsumesAttribute("application/json")); - var convention = GetConvention(); - - //Act - convention.Apply(action); - - // Assert - Assert.DoesNotContain(action.Selectors[0].EndpointMetadata, m => m is IAcceptsMetadata); - } - - private static EndpointMetadataConvention GetConvention(IServiceProvider services = null) - { - services ??= Mock.Of(); - return new EndpointMetadataConvention(services); - } - - private static ApplicationModelProviderContext GetContext(Type type) - { - var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); - var mvcOptions = Options.Create(new MvcOptions()); - var convention = new DefaultApplicationModelProvider(mvcOptions, new EmptyModelMetadataProvider()); - convention.OnProvidersExecuting(context); - - return context; - } - - private static ActionModel GetActionModel( - Type controllerType, - string actionName) - { - var context = GetContext(controllerType); - var controller = Assert.Single(context.Result.Controllers); - return Assert.Single(controller.Actions, m => m.ActionName == actionName); - } - - private class TestController - { - public ActionResult ActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; - public ActionResult ActionWithRemovalFromParameterMetadata(RemovesAcceptsParameterMetadata param1) => null; - public ActionResult ActionWithRemovalFromParameterEndpointMetadata(RemovesAcceptsParameterEndpointMetadata param1) => null; - - [HttpGet("selector1")] - [HttpGet("selector2")] - public ActionResult MultipleSelectorsActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; - - public AddsCustomEndpointMetadataResult ActionWithMetadataInResult() => null; - - public ValueTask ActionWithMetadataInValueTaskOfResult() - => ValueTask.FromResult(null); - - public Task ActionWithMetadataInTaskOfResult() - => Task.FromResult(null); - - [HttpGet("selector1")] - [HttpGet("selector2")] - public AddsCustomEndpointMetadataActionResult MultipleSelectorsActionWithMetadataInActionResult() => null; - - public AddsCustomEndpointMetadataActionResult ActionWithMetadataInActionResult() => null; - - public ValueTask ActionWithMetadataInValueTaskOfActionResult() - => ValueTask.FromResult(null); - - public Task ActionWithMetadataInTaskOfActionResult() - => Task.FromResult(null); - - public RemovesAcceptsMetadataResult ActionWithNoAcceptsMetadataInResult() => null; - - public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfResult() - => ValueTask.FromResult(null); - - public Task ActionWithNoAcceptsMetadataInTaskOfResult() - => Task.FromResult(null); - - public RemovesAcceptsMetadataActionResult ActionWithNoAcceptsMetadataInActionResult() => null; - - public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfActionResult() - => ValueTask.FromResult(null); - - public Task ActionWithNoAcceptsMetadataInTaskOfActionResult() - => Task.FromResult(null); - } - - private class CustomEndpointMetadata - { - public string Data { get; init; } - - public MetadataSource Source { get; init; } - } - private enum MetadataSource - { - Caller, - Parameter, - ReturnType - } - - private class ParameterNameMetadata - { - public string Name { get; init; } - } - - private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider - { - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) - { - parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); - } - - public static void PopulateMetadata(EndpointMetadataContext context) - { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); - } - } - - private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - - private class AddsCustomEndpointMetadataActionResult : IEndpointMetadataProvider, IActionResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); - } - public Task ExecuteResultAsync(ActionContext context) => throw new NotImplementedException(); - } - - private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - if (context.EndpointMetadata is not null) - { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = context.EndpointMetadata[i]; - if (metadata is IAcceptsMetadata) - { - context.EndpointMetadata.RemoveAt(i); - } - } - } - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - - private class RemovesAcceptsMetadataActionResult : IEndpointMetadataProvider, IActionResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - if (context.EndpointMetadata is not null) - { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = context.EndpointMetadata[i]; - if (metadata is IAcceptsMetadata) - { - context.EndpointMetadata.RemoveAt(i); - } - } - } - } - - public Task ExecuteResultAsync(ActionContext context) => throw new NotImplementedException(); - } - - private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider - { - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) - { - if (parameterContext.EndpointMetadata is not null) - { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = parameterContext.EndpointMetadata[i]; - if (metadata is IAcceptsMetadata) - { - parameterContext.EndpointMetadata.RemoveAt(i); - } - } - } - } - } - - private class RemovesAcceptsParameterEndpointMetadata : IEndpointMetadataProvider - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - if (context.EndpointMetadata is not null) - { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = context.EndpointMetadata[i]; - if (metadata is IAcceptsMetadata) - { - context.EndpointMetadata.RemoveAt(i); - } - } - } - } - } -} diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs new file mode 100644 index 000000000000..84e43647a399 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs @@ -0,0 +1,492 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels; + +public class EndpointMetadataProviderTest +{ + [Theory] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInActionResult))] + public void DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider(Type controllerType, string actionName) + { + // Act + var endpoint = GetEndpoint(controllerType, actionName); + + // Assert + Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); + } + + [Fact] + public void DiscoversEndpointMetadata_ForAllSelectors_FromReturnTypeImplementingIEndpointMetadataProvider() + { + // Act + var endpoints = GetEndpoints(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithMetadataInActionResult)); + + // Assert + Assert.Collection(endpoints, + endpoint => Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }), + endpoint => Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType })); + } + + [Fact] + public void DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() + { + // Act + var endpoint = GetEndpoint(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); + + // Assert + Assert.Contains(endpoint.Metadata, m => m is ParameterNameMetadata { Name: "param1" }); + } + + [Fact] + public void DiscoversEndpointMetadata_ForAllSelectors_FromParametersImplementingIEndpointParameterMetadataProvider() + { + // Act + var endpoints = GetEndpoints(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithParameterMetadata)); + + // Assert + Assert.Collection(endpoints, + endpoint => Assert.Contains(endpoint.Metadata, m => m is ParameterNameMetadata { Name: "param1" }), + endpoint => Assert.Contains(endpoint.Metadata, m => m is ParameterNameMetadata { Name: "param1" })); + } + + [Fact] + public void DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() + { + // Act + var endpoint = GetEndpoint(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); + + // Assert + Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); + } + + [Fact] + public void DiscoversEndpointMetadata_ForAllSelectors_FromParametersImplementingIEndpointMetadataProvider() + { + // Act + var endpoints = GetEndpoints(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithParameterMetadata)); + + // Assert + Assert.Collection(endpoints, + endpoint => Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }), + endpoint => Assert.Contains(endpoint.Metadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter })); + } + + [Fact] + public void DiscoversMetadata_CorrectOrder() + { + // Arrange + var dataSource = GetEndpointDataSource(typeof(TestController), nameof(TestController.ActionWithParameterMetadata)); + var routeGroupContext = new RouteGroupContext + { + Prefix = RoutePatternFactory.Parse("/"), + Conventions = new Action[] + { + builder => builder.Metadata.Add(new CustomEndpointMetadata() { Source = MetadataSource.Caller }), + }, + FinallyConventions = new Action[] + { + builder => builder.Metadata.Add(new CustomEndpointMetadata() { Source = MetadataSource.Finally }), + }, + }; + + // Act + var endpoint = Assert.Single(FilterEndpoints(dataSource.GetGroupedEndpoints(routeGroupContext))); + + // Assert + Assert.Collection( + endpoint.Metadata, + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller }), + m => Assert.True(m is ParameterNameMetadata { Name: "param1" }), + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Parameter }), + m => Assert.True(m is CustomAttribute), + m => Assert.True(m is ControllerActionDescriptor), + m => Assert.True(m is RouteNameMetadata), + m => Assert.True(m is SuppressLinkGenerationMetadata), + m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Finally })); + } + + [Theory] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInValueTaskOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInValueTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInActionResult))] + public void AllowsRemovalOfMetadata_ByReturnTypeImplementingIEndpointMetadataProvider(Type controllerType, string actionName) + { + // Arrange + var dataSource = GetEndpointDataSource(controllerType, actionName); + var routeGroupContext = new RouteGroupContext + { + Prefix = RoutePatternFactory.Parse("/"), + Conventions = new Action[] + { + builder => builder.Metadata.Add(new ConsumesAttribute("application/json")), + }, + }; + + // Act + var endpoint = Assert.Single(FilterEndpoints(dataSource.GetGroupedEndpoints(routeGroupContext))); + + // Assert + Assert.DoesNotContain(endpoint.Metadata, m => m is IAcceptsMetadata); + } + + [Fact] + public void AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointMetadataProvider() + { + // Arrange + var dataSource = GetEndpointDataSource(typeof(TestController), nameof(TestController.ActionWithRemovalFromParameterEndpointMetadata)); + var routeGroupContext = new RouteGroupContext + { + Prefix = RoutePatternFactory.Parse("/"), + Conventions = new Action[] + { + builder => builder.Metadata.Add(new ConsumesAttribute("application/json")), + }, + }; + + //Act + var endpoint = Assert.Single(FilterEndpoints(dataSource.GetGroupedEndpoints(routeGroupContext))); + + // Assert + Assert.DoesNotContain(endpoint.Metadata, m => m is IAcceptsMetadata); + } + + [Fact] + public void AllowsRemovalOfMetadata_ByParameterTypeImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var dataSource = GetEndpointDataSource(typeof(TestController), nameof(TestController.ActionWithRemovalFromParameterMetadata)); + var routeGroupContext = new RouteGroupContext + { + Prefix = RoutePatternFactory.Parse("/"), + Conventions = new Action[] + { + builder => builder.Metadata.Add(new ConsumesAttribute("application/json")), + }, + }; + + // Act + var endpoint = Assert.Single(FilterEndpoints(dataSource.GetGroupedEndpoints(routeGroupContext))); + + // Assert + Assert.DoesNotContain(endpoint.Metadata, m => m is IAcceptsMetadata); + } + + [Fact] + public void CanObserveRoutePattern_ForAllSelectors_FromParameterImplementingIEndpointetadataProvider() + { + // Act + var endpoints = GetEndpoints(typeof(TestController), nameof(TestController.MultipleSelectorsActionWithRoutePatternMetadata)); + + // Assert + Assert.Collection(endpoints, + endpoint => Assert.Contains(endpoint.Metadata, m => m is RoutePatternMetadata { RoutePattern: "selector1" }), + endpoint => Assert.Contains(endpoint.Metadata, m => m is RoutePatternMetadata { RoutePattern: "selector2" })); + } + + private Endpoint GetEndpoint(Type controllerType, string actionName) => Assert.Single(GetEndpoints(controllerType, actionName)); + private List GetEndpoints(Type controllerType, string actionName) => FilterEndpoints(GetEndpointDataSource(controllerType, actionName).Endpoints); + + // Filter out duplicate endpoints created by AddConventionalLinkGenerationRoute. + // These are added per route defined by MapControllerRoute rather than per action, so do not have inferred metadata. + private List FilterEndpoints(IReadOnlyList endpoints) + { + var nonLinkGenerationEndpoints = new List(); + + foreach (var endpoint in endpoints) + { + if (endpoint.Metadata is not [SuppressMatchingMetadata, ..]) + { + nonLinkGenerationEndpoints.Add(endpoint); + } + } + + return nonLinkGenerationEndpoints; + } + + private ControllerActionEndpointDataSource GetEndpointDataSource(Type controllerType, string actionName) + { + // Create ActionDescriptors how we normally would by default for the given controllerType + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(controllerType)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var options = Options.Create(new MvcOptions()); + var modelProvider = new DefaultApplicationModelProvider(options, new EmptyModelMetadataProvider()); + var controllerActionDescriptorProvider = new ControllerActionDescriptorProvider( + manager, + new ApplicationModelFactory(new[] { modelProvider }, options)); + + var actionDescriptorProviderContext = new ActionDescriptorProviderContext(); + controllerActionDescriptorProvider.OnProvidersExecuting(actionDescriptorProviderContext); + controllerActionDescriptorProvider.OnProvidersExecuted(actionDescriptorProviderContext); + + // Filter the ActionDescriptors by action name. + var descriptorsWithMatchingActionName = new List(); + + foreach (var descriptor in actionDescriptorProviderContext.Results) + { + if (descriptor is ControllerActionDescriptor cad && + cad.MethodInfo.Name == actionName) + { + descriptorsWithMatchingActionName.Add(cad); + } + } + + // Configure the ControllerActionEndpointDataSource to use our filtered ActionDescriptors for endpoint generation. + var actions = new MockActionDescriptorCollectionProvider(descriptorsWithMatchingActionName); + + var services = new ServiceCollection(); + services.AddSingleton(actions); + + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + services.Configure(routeOptionsSetup.Configure); + services.AddRouting(); + var serviceProvider = services.BuildServiceProvider(); + + var endpointFactory = new ActionEndpointFactory(serviceProvider.GetRequiredService(), Enumerable.Empty(), serviceProvider); + + var dataSource = new ControllerActionEndpointDataSource( + new ControllerActionEndpointDataSourceIdProvider(), + actions, + endpointFactory, + new OrderedEndpointsSequenceProvider()); + + // Add single route for non-attribute-routed actions. + dataSource.AddRoute("default", "/{controller}/{action}/{id?}", null, null, null); + + return dataSource; + } + + private sealed class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider + { + public MockActionDescriptorCollectionProvider(IReadOnlyList actions) + { + ActionDescriptors = new ActionDescriptorCollection(actions, 0); + } + + public ActionDescriptorCollection ActionDescriptors { get; } + } + + private class TestController + { + [Custom] + public ActionResult ActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; + public ActionResult ActionWithRemovalFromParameterMetadata(RemovesAcceptsParameterMetadata param1) => null; + public ActionResult ActionWithRemovalFromParameterEndpointMetadata(RemovesAcceptsParameterEndpointMetadata param1) => null; + + [HttpGet("selector1")] + [HttpGet("selector2")] + public ActionResult MultipleSelectorsActionWithParameterMetadata(AddsCustomParameterMetadata param1) => null; + + [HttpGet("selector1")] + [HttpGet("selector2")] + public ActionResult MultipleSelectorsActionWithRoutePatternMetadata(AddsRoutePatternMetadata param1) => null; + + public AddsCustomEndpointMetadataResult ActionWithMetadataInResult() => null; + + public ValueTask ActionWithMetadataInValueTaskOfResult() + => ValueTask.FromResult(null); + + public Task ActionWithMetadataInTaskOfResult() + => Task.FromResult(null); + + [HttpGet("selector1")] + [HttpGet("selector2")] + public AddsCustomEndpointMetadataActionResult MultipleSelectorsActionWithMetadataInActionResult() => null; + + public AddsCustomEndpointMetadataActionResult ActionWithMetadataInActionResult() => null; + + public ValueTask ActionWithMetadataInValueTaskOfActionResult() + => ValueTask.FromResult(null); + + public Task ActionWithMetadataInTaskOfActionResult() + => Task.FromResult(null); + + public RemovesAcceptsMetadataResult ActionWithNoAcceptsMetadataInResult() => null; + + public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfResult() + => ValueTask.FromResult(null); + + public Task ActionWithNoAcceptsMetadataInTaskOfResult() + => Task.FromResult(null); + + public RemovesAcceptsMetadataActionResult ActionWithNoAcceptsMetadataInActionResult() => null; + + public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfActionResult() + => ValueTask.FromResult(null); + + public Task ActionWithNoAcceptsMetadataInTaskOfActionResult() + => Task.FromResult(null); + } + + private class CustomEndpointMetadata + { + public string Data { get; init; } + + public MetadataSource Source { get; init; } + } + + private class ParameterNameMetadata + { + public string Name { get; init; } + } + + private class RoutePatternMetadata + { + public string RoutePattern { get; init; } + } + + private class CustomAttribute : Attribute + { + } + + private enum MetadataSource + { + Caller, + Parameter, + ReturnType, + Finally + } + + private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + { + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) + { + builder.Metadata.Add(new ParameterNameMetadata { Name = parameter.Name }); + } + + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); + } + } + + private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class AddsCustomEndpointMetadataActionResult : IEndpointMetadataProvider, IActionResult + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); + } + public Task ExecuteResultAsync(ActionContext context) => throw new NotImplementedException(); + } + + private class AddsRoutePatternMetadata : IEndpointMetadataProvider + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + if (builder is not RouteEndpointBuilder reb) + { + return; + } + + builder.Metadata.Add(new RoutePatternMetadata { RoutePattern = reb.RoutePattern.RawText }); + } + } + + private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + for (int i = builder.Metadata.Count - 1; i >= 0; i--) + { + var metadata = builder.Metadata[i]; + if (metadata is IAcceptsMetadata) + { + builder.Metadata.RemoveAt(i); + } + } + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class RemovesAcceptsMetadataActionResult : IEndpointMetadataProvider, IActionResult + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + if (builder.Metadata is not null) + { + for (int i = builder.Metadata.Count - 1; i >= 0; i--) + { + var metadata = builder.Metadata[i]; + if (metadata is IAcceptsMetadata) + { + builder.Metadata.RemoveAt(i); + } + } + } + } + + public Task ExecuteResultAsync(ActionContext context) => throw new NotImplementedException(); + } + + private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider + { + public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) + { + if (builder.Metadata is not null) + { + for (int i = builder.Metadata.Count - 1; i >= 0; i--) + { + var metadata = builder.Metadata[i]; + if (metadata is IAcceptsMetadata) + { + builder.Metadata.RemoveAt(i); + } + } + } + } + } + + private class RemovesAcceptsParameterEndpointMetadata : IEndpointMetadataProvider + { + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + if (builder.Metadata is not null) + { + for (int i = builder.Metadata.Count - 1; i >= 0; i--) + { + var metadata = builder.Metadata[i]; + if (metadata is IAcceptsMetadata) + { + builder.Metadata.RemoveAt(i); + } + } + } + } + } +} diff --git a/src/Shared/EndpointMetadataPopulator.cs b/src/Shared/EndpointMetadataPopulator.cs new file mode 100644 index 000000000000..bf36507d9d0c --- /dev/null +++ b/src/Shared/EndpointMetadataPopulator.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.Internal; + +#nullable enable + +namespace Microsoft.AspNetCore.Http; + +[UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Trimmer warnings are presented in RequestDelegateFactory.")] +internal static class EndpointMetadataPopulator +{ + private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; + + public static void PopulateMetadata(MethodInfo methodInfo, EndpointBuilder builder, IEnumerable? parameters = null) + { + object?[]? invokeArgs = null; + parameters ??= methodInfo.GetParameters(); + + // Get metadata from parameter types + foreach (var parameter in parameters) + { + if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointParameterMetadataProvider + invokeArgs ??= new object[2]; + invokeArgs[0] = parameter; + invokeArgs[1] = builder; + PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + + if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointMetadataProvider + invokeArgs ??= new object[2]; + invokeArgs[0] = methodInfo; + invokeArgs[1] = builder; + PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + } + + // Get metadata from return type + var returnType = methodInfo.ReturnType; + if (AwaitableInfo.IsTypeAwaitable(returnType, out var awaitableInfo)) + { + returnType = awaitableInfo.ResultType; + } + + if (returnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType)) + { + // Return type implements IEndpointMetadataProvider + invokeArgs ??= new object[2]; + invokeArgs[0] = methodInfo; + invokeArgs[1] = builder; + PopulateMetadataForEndpointMethod.MakeGenericMethod(returnType).Invoke(null, invokeArgs); + } + } + + private static void PopulateMetadataForParameter(ParameterInfo parameter, EndpointBuilder builder) + where T : IEndpointParameterMetadataProvider + { + T.PopulateMetadata(parameter, builder); + } + + private static void PopulateMetadataForEndpoint(MethodInfo method, EndpointBuilder builder) + where T : IEndpointMetadataProvider + { + T.PopulateMetadata(method, builder); + } +}