From d3268148c82a5eb68aebeaa5d889de65c1680990 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 28 Mar 2022 11:46:22 -0700 Subject: [PATCH 01/20] Add interfaces for providing metadata from route handler types - IProvideEndpointParameterMetadata - IProvideEndpointResponseMetadata - Contributes to #40646 --- .../src/IProvideEndpointParameterMetadata.cs | 22 +++++++++++++++++++ .../src/IProvideEndpointResponseMetadata.cs | 20 +++++++++++++++++ .../src/PublicAPI.Unshipped.txt | 4 ++++ 3 files changed, 46 insertions(+) create mode 100644 src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs create mode 100644 src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs diff --git a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs new file mode 100644 index 000000000000..2794de7945b7 --- /dev/null +++ b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// Indicates that a type provides a static method that returns metadata when declared as the +/// parameter type of a route handler delegate. The method must be of the form: +/// public static GetMetadata( parameter, services) +/// +public interface IProvideEndpointParameterMetadata +{ + /// + /// Supplies objects to apply as metadata to the related . + /// + /// The that represents the parameter to the endpoint's route handler delegate. + /// The application's . + /// The objects to apply as metadata. + public static abstract IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services); +} diff --git a/src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs new file mode 100644 index 000000000000..f22dd9879b80 --- /dev/null +++ b/src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Indicates that a type provides a static method that returns metadata when declared as the +/// returned type of a route handler delegate. The method must be of the form: +/// public static GetMetadata( endpoint, services) +/// +public interface IProvideEndpointResponseMetadata +{ + /// + /// Supplies objects to apply as metadata to the related . + /// + /// The the returned metadata will be applied to. + /// The application's . + /// The objects to apply as metadata. + public static abstract IEnumerable GetMetadata(Endpoint endpoint, IServiceProvider services); +} diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index f5825c4e8476..d3f732b1ae04 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,8 @@ #nullable enable +Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata +Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata.GetMetadata(System.Reflection.ParameterInfo! parameter, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.IProvideEndpointResponseMetadata +Microsoft.AspNetCore.Http.IProvideEndpointResponseMetadata.GetMetadata(Microsoft.AspNetCore.Http.Endpoint! endpoint, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions From 8b9eacdf16172659bad7961faafe6cab0f35d7e1 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 28 Mar 2022 13:02:50 -0700 Subject: [PATCH 02/20] Update RequestDelegateFactory to get metadata from method parameter, return & attribute types Contributes to #40646 --- ...etadata.cs => IProvideEndpointMetadata.cs} | 14 ++-- .../src/IProvideEndpointParameterMetadata.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 4 +- .../src/RequestDelegateFactory.cs | 77 +++++++++++++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) rename src/Http/Http.Extensions/src/{IProvideEndpointResponseMetadata.cs => IProvideEndpointMetadata.cs} (51%) diff --git a/src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs similarity index 51% rename from src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs rename to src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs index f22dd9879b80..533c6fbecfa7 100644 --- a/src/Http/Http.Extensions/src/IProvideEndpointResponseMetadata.cs +++ b/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs @@ -1,20 +1,22 @@ // 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; /// -/// Indicates that a type provides a static method that returns metadata when declared as the -/// returned type of a route handler delegate. The method must be of the form: -/// public static GetMetadata( endpoint, services) +/// Indicates that a type provides a static method that returns metadata when declared as a parameter type, type, or +/// the returned type of an route handler delegate. The method must be of the form: +/// public static GetMetadata( methodInfo, services) /// -public interface IProvideEndpointResponseMetadata +public interface IProvideEndpointMetadata { /// /// Supplies objects to apply as metadata to the related . /// - /// The the returned metadata will be applied to. + /// The representing the endpoint route handler delegate. /// The application's . /// The objects to apply as metadata. - public static abstract IEnumerable GetMetadata(Endpoint endpoint, IServiceProvider services); + public static abstract IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services); } diff --git a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs index 2794de7945b7..fcf50f2af4a2 100644 --- a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs +++ b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Http; /// /// Indicates that a type provides a static method that returns metadata when declared as the -/// parameter type of a route handler delegate. The method must be of the form: +/// parameter type of an route handler delegate. The method must be of the form: /// public static GetMetadata( parameter, services) /// public interface IProvideEndpointParameterMetadata diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index d3f732b1ae04..cf1420313549 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ #nullable enable Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata.GetMetadata(System.Reflection.ParameterInfo! parameter, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! -Microsoft.AspNetCore.Http.IProvideEndpointResponseMetadata -Microsoft.AspNetCore.Http.IProvideEndpointResponseMetadata.GetMetadata(Microsoft.AspNetCore.Http.Endpoint! endpoint, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.IProvideEndpointMetadata +Microsoft.AspNetCore.Http.IProvideEndpointMetadata.GetMetadata(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index c32a6a790147..487083ccbe98 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -40,6 +40,8 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!; private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(GetMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(GetMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -162,6 +164,7 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func new() { + ServiceProvider = options?.ServiceProvider, ServiceProviderIsService = options?.ServiceProvider?.GetService(), RouteParameters = options?.RouteParameterNames?.ToList(), ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, @@ -191,6 +194,9 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var returnType = methodInfo.ReturnType; factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments); + // Add metadata provided by method attribute, argument, and return types + AddTypeProvidedMetadata(methodInfo, factoryContext); + // 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.Filters is { Count: > 0 }) @@ -222,6 +228,76 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); } + private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContext factoryContext) + { + // Get metadata from parameter types + var parameters = methodInfo.GetParameters(); + foreach (var parameter in parameters) + { + if (typeof(IProvideEndpointParameterMetadata).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IProvideEndpointParameterMetadata + var metadata = GetMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); + if (metadata is null) + { + throw new ArgumentNullException(null, ""); + } + + factoryContext.Metadata.AddRange((IEnumerable)metadata); + } + + if (typeof(IProvideEndpointMetadata).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IProvideEndpointMetadata + var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); + if (metadata is null) + { + throw new ArgumentNullException(null, ""); + } + + factoryContext.Metadata.AddRange((IEnumerable)metadata); + } + } + + // Get metadata from return type + if (methodInfo.ReturnType is not null && typeof(IProvideEndpointMetadata).IsAssignableFrom(methodInfo.ReturnType)) + { + // Return type implements IProvideEndpointMetadata + var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); + if (metadata is null) + { + throw new ArgumentNullException(null, ""); + } + + factoryContext.Metadata.AddRange((IEnumerable)metadata); + } + + // Get metadata from method attributes that implement IProvideEndpointMetadata + var methodAttrs = methodInfo.GetCustomAttributes(inherit: true).OfType().ToList(); + foreach (var attribute in methodAttrs) + { + var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(attribute.GetType()).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); + if (metadata is null) + { + throw new ArgumentNullException(null, ""); + } + + factoryContext.Metadata.AddRange((IEnumerable)metadata); + } + } + + private static IEnumerable GetMetadataForParameter(ParameterInfo parameter, IServiceProvider services) + where T : IProvideEndpointParameterMetadata + { + return T.GetMetadata(parameter, services); + } + + private static IEnumerable GetMetadataForEndpoint(MethodInfo methodInfo, IServiceProvider services) + where T : IProvideEndpointMetadata + { + return T.GetMetadata(methodInfo, services); + } + private static RouteHandlerFilterDelegate CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext) { Debug.Assert(factoryContext.Filters is not null); @@ -1669,6 +1745,7 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex private class FactoryContext { // Options + public IServiceProvider? ServiceProvider { get; init; } public IServiceProviderIsService? ServiceProviderIsService { get; init; } public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } From d16183ef8e868d847133542a8d22e248f1ff4dc6 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 28 Mar 2022 14:08:08 -0700 Subject: [PATCH 03/20] Set exception message --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 487083ccbe98..651bc3da929e 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -230,6 +230,8 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContext factoryContext) { + const string argumentNullExceptionMessage = "The IEnumerable returned from GetMetadata must not be null."; + // Get metadata from parameter types var parameters = methodInfo.GetParameters(); foreach (var parameter in parameters) @@ -240,7 +242,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex var metadata = GetMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); if (metadata is null) { - throw new ArgumentNullException(null, ""); + throw new ArgumentNullException(null, argumentNullExceptionMessage); } factoryContext.Metadata.AddRange((IEnumerable)metadata); @@ -252,7 +254,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); if (metadata is null) { - throw new ArgumentNullException(null, ""); + throw new ArgumentNullException(null, argumentNullExceptionMessage); } factoryContext.Metadata.AddRange((IEnumerable)metadata); @@ -266,7 +268,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); if (metadata is null) { - throw new ArgumentNullException(null, ""); + throw new ArgumentNullException(null, argumentNullExceptionMessage); } factoryContext.Metadata.AddRange((IEnumerable)metadata); @@ -279,7 +281,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(attribute.GetType()).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); if (metadata is null) { - throw new ArgumentNullException(null, ""); + throw new ArgumentNullException(null, argumentNullExceptionMessage); } factoryContext.Metadata.AddRange((IEnumerable)metadata); From 336b7903f387f7f2c790f5b0606f973581ffd612 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 28 Mar 2022 14:56:14 -0700 Subject: [PATCH 04/20] Add unit tests for metadata interfaces --- .../src/RequestDelegateFactory.cs | 2 +- .../test/RequestDelegateFactoryTests.cs | 201 ++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 651bc3da929e..206c163c7030 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -251,7 +251,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex if (typeof(IProvideEndpointMetadata).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IProvideEndpointMetadata - var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); + var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); if (metadata is null) { throw new ArgumentNullException(null, argumentNullExceptionMessage); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index f597d15edc15..2f3c8461e6ce 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4560,6 +4560,94 @@ string HelloName(string name) Assert.Equal("HELLO, TESTNAMEPREFIX!", responseBody); } + [Fact] + public void RequestDelegateFactory_DiscoversEndpointParameterMetadata_FromParamaters() + { + var httpContext = CreateHttpContext(); + + var @delegate = (ProvidesParameterMetadata param1, ProvidesParameterMetadata param2) => { }; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var metadata = factoryResult.EndpointMetadata; + + Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param1", StringComparison.Ordinal)); + Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param2", StringComparison.Ordinal)); + } + + [Fact] + public void RequestDelegateFactory_DiscoversEndpointMetadata_FromParameters() + { + var httpContext = CreateHttpContext(); + + var @delegate = (ProvidesParameterMetadata param1) => { }; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var metadata = factoryResult.EndpointMetadata; + + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); + } + + [Fact] + public void RequestDelegateFactory_DiscoversEndpointMetadata_FromReturnType() + { + var httpContext = CreateHttpContext(); + + var @delegate = () => new ProvidesEndpointResultMetadata(); + var factoryResult = RequestDelegateFactory.Create(@delegate); + var metadata = factoryResult.EndpointMetadata; + + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); + } + + [Fact] + public void RequestDelegateFactory_DiscoversEndpointMetadata_FromMethodAttributes() + { + var httpContext = CreateHttpContext(); + + var @delegate = [ProvidesMetadataAttribute("Some custom data")] () => { }; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var metadata = factoryResult.EndpointMetadata; + + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.MethodAttribute && string.Equals("Some custom data", cem.Data, StringComparison.Ordinal)); + } + + [Fact] + public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromParameters() + { + var httpContext = CreateHttpContext(); + + var @delegate = (ProvidesNullParameterMetadata param1, ProvidesParameterMetadata param2) => { }; + + Assert.Throws(() => + { + var factoryResult = RequestDelegateFactory.Create(@delegate); + }); + } + + [Fact] + public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromReturnType() + { + var httpContext = CreateHttpContext(); + + var @delegate = () => new ProvidesNullEndpointResultMetadata(); + + Assert.Throws(() => + { + var factoryResult = RequestDelegateFactory.Create(@delegate); + }); + } + + [Fact] + public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromMethodAttributes() + { + var httpContext = CreateHttpContext(); + + var @delegate = [ProvidesNullMetadataAttribute("Some custom data")] () => { }; + + Assert.Throws(() => + { + var factoryResult = RequestDelegateFactory.Create(@delegate); + }); + } + private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); @@ -4576,6 +4664,119 @@ private DefaultHttpContext CreateHttpContext() }; } + private class ProvidesEndpointResultMetadata : IProvideEndpointMetadata, IResult + { + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { + yield return new CustomEndpointMetadata { Source = MetadataSource.ReturnType }; + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class ProvidesNullEndpointResultMetadata : IProvideEndpointMetadata, IResult + { + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class ProvidesParameterMetadata : IProvideEndpointParameterMetadata, IProvideEndpointMetadata + { + public static IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services) + { + yield return new ParameterNameMetadata { Name = parameter.Name }; + } + + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { + yield return new CustomEndpointMetadata { Source = MetadataSource.Parameter }; + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class ProvidesNullParameterMetadata : IProvideEndpointParameterMetadata, IProvideEndpointMetadata + { + public static IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class ParameterNameMetadata + { + public string? Name { get; init; } + } + + private class CustomEndpointMetadata + { + public string? Data { get; init; } + + public MetadataSource Source { get; init; } + } + + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + private sealed class ProvidesMetadataAttribute : Attribute, IProvideEndpointMetadata + { + public ProvidesMetadataAttribute(string someData) + { + SomeData = someData; + } + + public string SomeData { get; init; } + + public MetadataSource Source => MetadataSource.MethodAttribute; + + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { + yield return new CustomEndpointMetadata { Source = MetadataSource.MethodAttribute, Data = methodInfo.GetCustomAttribute()?.SomeData }; + } + } + + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + private sealed class ProvidesNullMetadataAttribute : Attribute, IProvideEndpointMetadata + { + public ProvidesNullMetadataAttribute(string someData) + { + SomeData = someData; + } + + public string SomeData { get; init; } + + public MetadataSource Source => MetadataSource.MethodAttribute; + + public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + { +#pragma warning disable CS8603 // Possible null reference return. + return null; +#pragma warning restore CS8603 // Possible null reference return. + } + } + + private enum MetadataSource + { + Parameter, + ReturnType, + MethodAttribute + } + private class Todo : ITodo { public int Id { get; set; } From 57652042a52a789c29cde05f59b69079c61136f4 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 28 Mar 2022 17:11:07 -0700 Subject: [PATCH 05/20] PR feedback --- .../src/RequestDelegateFactory.cs | 2 +- .../test/RequestDelegateFactoryTests.cs | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 206c163c7030..64d353cdee2c 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -275,7 +275,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex } // Get metadata from method attributes that implement IProvideEndpointMetadata - var methodAttrs = methodInfo.GetCustomAttributes(inherit: true).OfType().ToList(); + var methodAttrs = methodInfo.GetCustomAttributes(inherit: true).OfType(); foreach (var attribute in methodAttrs) { var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(attribute.GetType()).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 2f3c8461e6ce..310b63c0f69a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4561,7 +4561,7 @@ string HelloName(string name) } [Fact] - public void RequestDelegateFactory_DiscoversEndpointParameterMetadata_FromParamaters() + public void RequestDelegateFactory_DiscoversEndpointParameterMetadata_FromParameters() { var httpContext = CreateHttpContext(); @@ -4678,9 +4678,7 @@ private class ProvidesNullEndpointResultMetadata : IProvideEndpointMetadata, IRe { public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) { -#pragma warning disable CS8603 // Possible null reference return. - return null; -#pragma warning restore CS8603 // Possible null reference return. + return null!; } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); @@ -4705,16 +4703,12 @@ private class ProvidesNullParameterMetadata : IProvideEndpointParameterMetadata, { public static IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services) { -#pragma warning disable CS8603 // Possible null reference return. - return null; -#pragma warning restore CS8603 // Possible null reference return. + return null!; } public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) { -#pragma warning disable CS8603 // Possible null reference return. - return null; -#pragma warning restore CS8603 // Possible null reference return. + return null!; } public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; @@ -4764,9 +4758,7 @@ public ProvidesNullMetadataAttribute(string someData) public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) { -#pragma warning disable CS8603 // Possible null reference return. - return null; -#pragma warning restore CS8603 // Possible null reference return. + return null!; } } From 784440baec26fd84681cb643839ab2d977d4ed0f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 30 Mar 2022 10:49:25 -0700 Subject: [PATCH 06/20] Remove gathering metadata from method attributes --- .../src/RequestDelegateFactory.cs | 13 ---- .../test/RequestDelegateFactoryTests.cs | 61 ------------------- 2 files changed, 74 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 64d353cdee2c..ab90b7e8ab5e 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -273,19 +273,6 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContex factoryContext.Metadata.AddRange((IEnumerable)metadata); } - - // Get metadata from method attributes that implement IProvideEndpointMetadata - var methodAttrs = methodInfo.GetCustomAttributes(inherit: true).OfType(); - foreach (var attribute in methodAttrs) - { - var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(attribute.GetType()).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); - if (metadata is null) - { - throw new ArgumentNullException(null, argumentNullExceptionMessage); - } - - factoryContext.Metadata.AddRange((IEnumerable)metadata); - } } private static IEnumerable GetMetadataForParameter(ParameterInfo parameter, IServiceProvider services) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 310b63c0f69a..9d549256d71b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4597,18 +4597,6 @@ public void RequestDelegateFactory_DiscoversEndpointMetadata_FromReturnType() Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); } - [Fact] - public void RequestDelegateFactory_DiscoversEndpointMetadata_FromMethodAttributes() - { - var httpContext = CreateHttpContext(); - - var @delegate = [ProvidesMetadataAttribute("Some custom data")] () => { }; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var metadata = factoryResult.EndpointMetadata; - - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.MethodAttribute && string.Equals("Some custom data", cem.Data, StringComparison.Ordinal)); - } - [Fact] public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromParameters() { @@ -4635,19 +4623,6 @@ public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedF }); } - [Fact] - public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromMethodAttributes() - { - var httpContext = CreateHttpContext(); - - var @delegate = [ProvidesNullMetadataAttribute("Some custom data")] () => { }; - - Assert.Throws(() => - { - var factoryResult = RequestDelegateFactory.Create(@delegate); - }); - } - private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); @@ -4726,42 +4701,6 @@ private class CustomEndpointMetadata public MetadataSource Source { get; init; } } - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - private sealed class ProvidesMetadataAttribute : Attribute, IProvideEndpointMetadata - { - public ProvidesMetadataAttribute(string someData) - { - SomeData = someData; - } - - public string SomeData { get; init; } - - public MetadataSource Source => MetadataSource.MethodAttribute; - - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) - { - yield return new CustomEndpointMetadata { Source = MetadataSource.MethodAttribute, Data = methodInfo.GetCustomAttribute()?.SomeData }; - } - } - - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - private sealed class ProvidesNullMetadataAttribute : Attribute, IProvideEndpointMetadata - { - public ProvidesNullMetadataAttribute(string someData) - { - SomeData = someData; - } - - public string SomeData { get; init; } - - public MetadataSource Source => MetadataSource.MethodAttribute; - - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) - { - return null!; - } - } - private enum MetadataSource { Parameter, From 28b1bea1aa6921323548688e01a923b71a6e3f66 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 30 Mar 2022 10:57:23 -0700 Subject: [PATCH 07/20] Remove public access modifier on static abstract method definitions --- src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs | 2 +- .../Http.Extensions/src/IProvideEndpointParameterMetadata.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs index 533c6fbecfa7..a02ceaf73b2e 100644 --- a/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs +++ b/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs @@ -18,5 +18,5 @@ public interface IProvideEndpointMetadata /// The representing the endpoint route handler delegate. /// The application's . /// The objects to apply as metadata. - public static abstract IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services); + static abstract IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services); } diff --git a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs index fcf50f2af4a2..6364db2d0f8e 100644 --- a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs +++ b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs @@ -18,5 +18,5 @@ public interface IProvideEndpointParameterMetadata /// The that represents the parameter to the endpoint's route handler delegate. /// The application's . /// The objects to apply as metadata. - public static abstract IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services); + static abstract IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services); } From 2e46a26020d7c3f91038130f8eab0fc4459d0d31 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 13:30:07 -0700 Subject: [PATCH 08/20] API review feedback --- .../src/EndpointMetadataContext.cs | 15 ++++ .../src/EndpointParameterMetadataContext.cs | 15 ++++ .../src/IEndpointMetadataProvider.cs | 17 ++++ .../src/IEndpointParameterMetadataProvider.cs | 17 ++++ .../src/IProvideEndpointMetadata.cs | 22 ------ .../src/IProvideEndpointParameterMetadata.cs | 22 ------ .../src/PublicAPI.Unshipped.txt | 8 +- .../src/RequestDelegateFactory.cs | 77 +++++++++++-------- .../test/RequestDelegateFactoryTests.cs | 69 ++++++----------- 9 files changed, 135 insertions(+), 127 deletions(-) create mode 100644 src/Http/Http.Extensions/src/EndpointMetadataContext.cs create mode 100644 src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs create mode 100644 src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs create mode 100644 src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs delete mode 100644 src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs delete mode 100644 src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs new file mode 100644 index 000000000000..540d1de07eae --- /dev/null +++ b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs @@ -0,0 +1,15 @@ +// 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; + +public sealed class EndpointMetadataContext +{ + public MethodInfo Method { get; init; } + + public IServiceProvider? Services { get; init; } + + public IList EndpointMetadata { get; init; } +} diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs new file mode 100644 index 000000000000..e2f59e530533 --- /dev/null +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -0,0 +1,15 @@ +// 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; + +public class EndpointParameterMetadataContext +{ + public ParameterInfo Parameter { get; internal set; } + + public IServiceProvider? Services { get; init; } + + public IList EndpointMetadata { get; init; } +} diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs new file mode 100644 index 000000000000..69e7df46d953 --- /dev/null +++ b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs @@ -0,0 +1,17 @@ +// 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; + +/// +/// Indicates that a type provides a static method that returns metadata when declared as a parameter type, +/// type, or the returned type of an route handler delegate. +/// +public interface IEndpointMetadataProvider +{ + /// + /// Supplies objects to apply as metadata to the related . + /// + /// 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 new file mode 100644 index 000000000000..2968dcb1da34 --- /dev/null +++ b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs @@ -0,0 +1,17 @@ +// 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; + +/// +/// Indicates that a type provides a static method that returns metadata when declared as the +/// parameter type of an route handler delegate. +/// +public interface IEndpointParameterMetadataProvider +{ + /// + /// Populates metadata for the related . + /// + /// The . + static abstract void PopulateMetadata(EndpointParameterMetadataContext parameterContext); +} diff --git a/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs deleted file mode 100644 index a02ceaf73b2e..000000000000 --- a/src/Http/Http.Extensions/src/IProvideEndpointMetadata.cs +++ /dev/null @@ -1,22 +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; - -/// -/// Indicates that a type provides a static method that returns metadata when declared as a parameter type, type, or -/// the returned type of an route handler delegate. The method must be of the form: -/// public static GetMetadata( methodInfo, services) -/// -public interface IProvideEndpointMetadata -{ - /// - /// Supplies objects to apply as metadata to the related . - /// - /// The representing the endpoint route handler delegate. - /// The application's . - /// The objects to apply as metadata. - static abstract IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services); -} diff --git a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs b/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs deleted file mode 100644 index 6364db2d0f8e..000000000000 --- a/src/Http/Http.Extensions/src/IProvideEndpointParameterMetadata.cs +++ /dev/null @@ -1,22 +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; - -/// -/// Indicates that a type provides a static method that returns metadata when declared as the -/// parameter type of an route handler delegate. The method must be of the form: -/// public static GetMetadata( parameter, services) -/// -public interface IProvideEndpointParameterMetadata -{ - /// - /// Supplies objects to apply as metadata to the related . - /// - /// The that represents the parameter to the endpoint's route handler delegate. - /// The application's . - /// The objects to apply as metadata. - static abstract IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services); -} diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index cf1420313549..fc77987edb4a 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ #nullable enable -Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata -Microsoft.AspNetCore.Http.IProvideEndpointParameterMetadata.GetMetadata(System.Reflection.ParameterInfo! parameter, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! -Microsoft.AspNetCore.Http.IProvideEndpointMetadata -Microsoft.AspNetCore.Http.IProvideEndpointMetadata.GetMetadata(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider! services) -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider +Microsoft.AspNetCore.Http.IEndpointMetadataProvider +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index ab90b7e8ab5e..60a42a0fe686 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -40,8 +40,8 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!; private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo GetMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(GetMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo GetMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(GetMetadataForEndpoint), 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)!; // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -194,7 +194,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var returnType = methodInfo.ReturnType; factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments); - // Add metadata provided by method attribute, argument, and return types + // Add metadata provided by method argument and return types AddTypeProvidedMetadata(methodInfo, factoryContext); // If there are filters registered on the route handler, then we update the method call and @@ -230,61 +230,70 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContext factoryContext) { - const string argumentNullExceptionMessage = "The IEnumerable returned from GetMetadata must not be null."; + EndpointParameterMetadataContext? parameterContext = null; + EndpointMetadataContext? context = null; + object?[]? invokeArgs = null; // Get metadata from parameter types var parameters = methodInfo.GetParameters(); foreach (var parameter in parameters) { - if (typeof(IProvideEndpointParameterMetadata).IsAssignableFrom(parameter.ParameterType)) + if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { - // Parameter type implements IProvideEndpointParameterMetadata - var metadata = GetMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { parameter, factoryContext.ServiceProvider }); - if (metadata is null) + // Parameter type implements IEndpointParameterMetadataProvider + parameterContext ??= new EndpointParameterMetadataContext { - throw new ArgumentNullException(null, argumentNullExceptionMessage); - } - - factoryContext.Metadata.AddRange((IEnumerable)metadata); + EndpointMetadata = factoryContext.Metadata, + Parameter = parameter, + Services = factoryContext.ServiceProvider + }; + parameterContext.Parameter = parameter; + invokeArgs ??= new object[1]; + invokeArgs[0] = parameterContext; + PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); } - if (typeof(IProvideEndpointMetadata).IsAssignableFrom(parameter.ParameterType)) + if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { - // Parameter type implements IProvideEndpointMetadata - var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); - if (metadata is null) + // Parameter type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext { - throw new ArgumentNullException(null, argumentNullExceptionMessage); - } - - factoryContext.Metadata.AddRange((IEnumerable)metadata); + EndpointMetadata = factoryContext.Metadata, + Method = methodInfo, + Services = factoryContext.ServiceProvider + }; + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); } } // Get metadata from return type - if (methodInfo.ReturnType is not null && typeof(IProvideEndpointMetadata).IsAssignableFrom(methodInfo.ReturnType)) + if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) { - // Return type implements IProvideEndpointMetadata - var metadata = GetMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, new object?[] { methodInfo, factoryContext.ServiceProvider }); - if (metadata is null) + // Return type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext { - throw new ArgumentNullException(null, argumentNullExceptionMessage); - } - - factoryContext.Metadata.AddRange((IEnumerable)metadata); + EndpointMetadata = factoryContext.Metadata, + Method = methodInfo, + Services = factoryContext.ServiceProvider + }; + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, invokeArgs); } } - private static IEnumerable GetMetadataForParameter(ParameterInfo parameter, IServiceProvider services) - where T : IProvideEndpointParameterMetadata + private static void PopulateMetadataForParameter(EndpointParameterMetadataContext parameterContext) + where T : IEndpointParameterMetadataProvider { - return T.GetMetadata(parameter, services); + T.PopulateMetadata(parameterContext); } - private static IEnumerable GetMetadataForEndpoint(MethodInfo methodInfo, IServiceProvider services) - where T : IProvideEndpointMetadata + private static void PopulateMetadataForEndpoint(EndpointMetadataContext context) + where T : IEndpointMetadataProvider { - return T.GetMetadata(methodInfo, services); + T.PopulateMetadata(context); } private static RouteHandlerFilterDelegate CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 9d549256d71b..8e3947cccbd0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4597,31 +4597,10 @@ public void RequestDelegateFactory_DiscoversEndpointMetadata_FromReturnType() Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); } - [Fact] - public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromParameters() - { - var httpContext = CreateHttpContext(); - - var @delegate = (ProvidesNullParameterMetadata param1, ProvidesParameterMetadata param2) => { }; - - Assert.Throws(() => - { - var factoryResult = RequestDelegateFactory.Create(@delegate); - }); - } - - [Fact] - public void RequestDelegateFactory_ThrowsArgumentNullException_WhenNullReturnedFromIProvideEndpointMetadata_FromReturnType() - { - var httpContext = CreateHttpContext(); - - var @delegate = () => new ProvidesNullEndpointResultMetadata(); - - Assert.Throws(() => - { - var factoryResult = RequestDelegateFactory.Create(@delegate); - }); - } + // TODO: Add tests for: + // - Ordering of calls + // - Removing metadata + // - Accessing default metadata private DefaultHttpContext CreateHttpContext() { @@ -4639,54 +4618,54 @@ private DefaultHttpContext CreateHttpContext() }; } - private class ProvidesEndpointResultMetadata : IProvideEndpointMetadata, IResult + private class ProvidesEndpointResultMetadata : IEndpointMetadataProvider, IResult { - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + public static void PopulateMetadata(EndpointMetadataContext context) { - yield return new CustomEndpointMetadata { Source = MetadataSource.ReturnType }; + context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } - private class ProvidesNullEndpointResultMetadata : IProvideEndpointMetadata, IResult + private class ProvidesNullEndpointResultMetadata : IEndpointMetadataProvider, IResult { - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + public static void PopulateMetadata(EndpointMetadataContext context) { - return null!; + } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } - private class ProvidesParameterMetadata : IProvideEndpointParameterMetadata, IProvideEndpointMetadata + private class ProvidesParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { - public static IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services) + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) { - yield return new ParameterNameMetadata { Name = parameter.Name }; + parameterContext.EndpointMetadata.Add(new ParameterNameMetadata { Name = parameterContext.Parameter.Name }); } - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + public static void PopulateMetadata(EndpointMetadataContext context) { - yield return new CustomEndpointMetadata { Source = MetadataSource.Parameter }; + context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } - - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; } - private class ProvidesNullParameterMetadata : IProvideEndpointParameterMetadata, IProvideEndpointMetadata + private class ProvidesNullParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { - public static IEnumerable GetMetadata(ParameterInfo parameter, IServiceProvider services) + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) { - return null!; + } - public static IEnumerable GetMetadata(MethodInfo methodInfo, IServiceProvider services) + public static void PopulateMetadata(EndpointMetadataContext context) { - return null!; - } - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } } private class ParameterNameMetadata From 0481d3b4daf3c08c90517e1f48b04262d838613f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 18:30:59 -0700 Subject: [PATCH 09/20] WIP --- .../src/EndpointMetadataContext.cs | 15 - .../src/PublicAPI.Unshipped.txt | 4 - .../src/RequestDelegateFactory.cs | 74 ----- .../test/RequestDelegateFactoryTests.cs | 111 ------- .../Builder/EndpointRouteBuilderExtensions.cs | 74 +++++ .../Routing/src/EndpointMetadataContext.cs | 40 +++ .../src/EndpointParameterMetadataContext.cs | 14 +- .../src/IEndpointMetadataProvider.cs | 0 .../src/IEndpointParameterMetadataProvider.cs | 0 src/Http/Routing/src/PublicAPI.Unshipped.txt | 16 + ...egateEndpointRouteBuilderExtensionsTest.cs | 309 ++++++++++++++++++ 11 files changed, 452 insertions(+), 205 deletions(-) delete mode 100644 src/Http/Http.Extensions/src/EndpointMetadataContext.cs create mode 100644 src/Http/Routing/src/EndpointMetadataContext.cs rename src/Http/{Http.Extensions => Routing}/src/EndpointParameterMetadataContext.cs (58%) rename src/Http/{Http.Extensions => Routing}/src/IEndpointMetadataProvider.cs (100%) rename src/Http/{Http.Extensions => Routing}/src/IEndpointParameterMetadataProvider.cs (100%) diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs deleted file mode 100644 index 540d1de07eae..000000000000 --- a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs +++ /dev/null @@ -1,15 +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; - -public sealed class EndpointMetadataContext -{ - public MethodInfo Method { get; init; } - - public IServiceProvider? Services { get; init; } - - public IList EndpointMetadata { get; init; } -} diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index fc77987edb4a..f5825c4e8476 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,8 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider -Microsoft.AspNetCore.Http.IEndpointMetadataProvider -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 60a42a0fe686..c40202a2a8ea 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -40,8 +40,6 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!; private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), 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)!; // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -164,7 +162,6 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func new() { - ServiceProvider = options?.ServiceProvider, ServiceProviderIsService = options?.ServiceProvider?.GetService(), RouteParameters = options?.RouteParameterNames?.ToList(), ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, @@ -194,9 +191,6 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var returnType = methodInfo.ReturnType; factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments); - // Add metadata provided by method argument and return types - AddTypeProvidedMetadata(methodInfo, factoryContext); - // 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.Filters is { Count: > 0 }) @@ -228,74 +222,6 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); } - private static void AddTypeProvidedMetadata(MethodInfo methodInfo, FactoryContext factoryContext) - { - EndpointParameterMetadataContext? parameterContext = null; - EndpointMetadataContext? context = null; - object?[]? invokeArgs = null; - - // Get metadata from parameter types - var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - { - if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointParameterMetadataProvider - parameterContext ??= new EndpointParameterMetadataContext - { - EndpointMetadata = factoryContext.Metadata, - Parameter = parameter, - Services = factoryContext.ServiceProvider - }; - parameterContext.Parameter = parameter; - invokeArgs ??= new object[1]; - invokeArgs[0] = parameterContext; - PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - - if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext - { - EndpointMetadata = factoryContext.Metadata, - Method = methodInfo, - Services = factoryContext.ServiceProvider - }; - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - } - - // Get metadata from return type - if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) - { - // Return type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext - { - EndpointMetadata = factoryContext.Metadata, - Method = methodInfo, - Services = factoryContext.ServiceProvider - }; - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.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 RouteHandlerFilterDelegate CreateFilterPipeline(MethodInfo methodInfo, Expression? target, FactoryContext factoryContext) { Debug.Assert(factoryContext.Filters is not null); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 8e3947cccbd0..f597d15edc15 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4560,48 +4560,6 @@ string HelloName(string name) Assert.Equal("HELLO, TESTNAMEPREFIX!", responseBody); } - [Fact] - public void RequestDelegateFactory_DiscoversEndpointParameterMetadata_FromParameters() - { - var httpContext = CreateHttpContext(); - - var @delegate = (ProvidesParameterMetadata param1, ProvidesParameterMetadata param2) => { }; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var metadata = factoryResult.EndpointMetadata; - - Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param1", StringComparison.Ordinal)); - Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param2", StringComparison.Ordinal)); - } - - [Fact] - public void RequestDelegateFactory_DiscoversEndpointMetadata_FromParameters() - { - var httpContext = CreateHttpContext(); - - var @delegate = (ProvidesParameterMetadata param1) => { }; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var metadata = factoryResult.EndpointMetadata; - - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); - } - - [Fact] - public void RequestDelegateFactory_DiscoversEndpointMetadata_FromReturnType() - { - var httpContext = CreateHttpContext(); - - var @delegate = () => new ProvidesEndpointResultMetadata(); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var metadata = factoryResult.EndpointMetadata; - - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); - } - - // TODO: Add tests for: - // - Ordering of calls - // - Removing metadata - // - Accessing default metadata - private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); @@ -4618,75 +4576,6 @@ private DefaultHttpContext CreateHttpContext() }; } - private class ProvidesEndpointResultMetadata : 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 ProvidesNullEndpointResultMetadata : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - - private class ProvidesParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - - 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 ProvidesNullParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) - { - - } - - public static void PopulateMetadata(EndpointMetadataContext context) - { - - } - } - - private class ParameterNameMetadata - { - public string? Name { get; init; } - } - - private class CustomEndpointMetadata - { - public string? Data { get; init; } - - public MetadataSource Source { get; init; } - } - - private enum MetadataSource - { - Parameter, - ReturnType, - MethodAttribute - } - private class Todo : ITodo { public int Id { get; set; } diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index d8990104cb12..7e6dc1af5121 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -24,6 +24,8 @@ public static class EndpointRouteBuilderExtensions private static readonly string[] PutVerb = new[] { HttpMethods.Put }; private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; + private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointRouteBuilderExtensions).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointRouteBuilderExtensions).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; /// /// Adds a to the that matches HTTP GET requests @@ -544,9 +546,81 @@ private static RouteHandlerBuilder Map( endpointBuilder.Metadata.Add(attribute); } } + + // Add metadata provided by the delegate return type and parameter types + AddTypeProvidedMetadata(handler.Method, endpointBuilder.Metadata, endpoints.ServiceProvider); + endpointBuilder.RequestDelegate = filteredRequestDelegateResult.RequestDelegate; }); return routeHandlerBuilder; } + + private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList metadata, IServiceProvider? services) + { + EndpointParameterMetadataContext? parameterContext = null; + EndpointMetadataContext? context = null; + object?[]? invokeArgs = null; + + // Get metadata from parameter types + var parameters = methodInfo.GetParameters(); + foreach (var parameter in parameters) + { + if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointParameterMetadataProvider + parameterContext ??= new EndpointParameterMetadataContext + { + EndpointMetadata = metadata, + Parameter = parameter, + Services = services + }; + parameterContext.Parameter = parameter; + invokeArgs ??= new object[1]; + invokeArgs[0] = parameterContext; + PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + + if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext + { + EndpointMetadata = metadata, + Method = methodInfo, + Services = services + }; + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + } + + // Get metadata from return type + if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) + { + // Return type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext + { + EndpointMetadata = metadata, + Method = methodInfo, + Services = services + }; + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.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); + } } diff --git a/src/Http/Routing/src/EndpointMetadataContext.cs b/src/Http/Routing/src/EndpointMetadataContext.cs new file mode 100644 index 000000000000..68fc787825be --- /dev/null +++ b/src/Http/Routing/src/EndpointMetadataContext.cs @@ -0,0 +1,40 @@ +// 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; + +/// +/// Represents the information accessible during endpoint creation by types that implement . +/// +public sealed class EndpointMetadataContext +{ + /// + /// Creates a new instance of the . + /// + /// The associated with the current route handler. + /// The instance used to access application services + /// The objects that will be added to the endpoint's . + public EndpointMetadataContext(MethodInfo method, IServiceProvider? services, IList endpointMetadata) + { + Method = method; + Services = services; + EndpointMetadata = endpointMetadata; + } + + /// + /// Gets the associated with the current route handler. + /// + public MethodInfo Method { get; } + + /// + /// Gets the instance used to access application services. + /// + public IServiceProvider? Services { get; } + + /// + /// Gets the objects that will be added to the endpoint's . + /// + public IList EndpointMetadata { get; } +} diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Routing/src/EndpointParameterMetadataContext.cs similarity index 58% rename from src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs rename to src/Http/Routing/src/EndpointParameterMetadataContext.cs index e2f59e530533..cd462044f0c5 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Routing/src/EndpointParameterMetadataContext.cs @@ -5,11 +5,23 @@ namespace Microsoft.AspNetCore.Http; +/// +/// +/// public class EndpointParameterMetadataContext { - public ParameterInfo Parameter { get; internal set; } + /// + /// + /// + public ParameterInfo Parameter { get; internal set; } // internal set to allow re-use + /// + /// + /// public IServiceProvider? Services { get; init; } + /// + /// + /// public IList EndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Routing/src/IEndpointMetadataProvider.cs similarity index 100% rename from src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs rename to src/Http/Routing/src/IEndpointMetadataProvider.cs diff --git a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs b/src/Http/Routing/src/IEndpointParameterMetadataProvider.cs similarity index 100% rename from src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs rename to src/Http/Routing/src/IEndpointParameterMetadataProvider.cs diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 432fe7abd574..a30153fa8e23 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -1,4 +1,20 @@ #nullable enable +Microsoft.AspNetCore.Http.EndpointMetadataContext +Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void +Microsoft.AspNetCore.Http.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! +Microsoft.AspNetCore.Http.EndpointMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.init -> void +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.init -> void +Microsoft.AspNetCore.Http.IEndpointMetadataProvider +Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token, System.Type! type) -> void Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token) -> void diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index df21fbb1d7f1..ed1007377b7a 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -3,6 +3,7 @@ using System.IO.Pipelines; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -221,6 +222,179 @@ public void AddingMetadataAfterBuildingEndpointThrows(Func(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo"))); } + [Fact] + public void Map_DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = (AddsCustomParameterMetadata param1, AddsCustomParameterMetadata param2) => { }; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param1", StringComparison.Ordinal)); + Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param2", StringComparison.Ordinal)); + } + + [Fact] + public void Map_DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = (AddsCustomParameterMetadata param1) => { }; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); + } + + [Fact] + public void Map_DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = () => new AddsCustomEndpointMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); + } + + [Fact] + public void Map_ProvidesDefaultMethodInfoMetadata_ToReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is MethodInfo); + } + + [Fact] + public void Map_ProvidesDefaultMethodAttributeMetadata_ToReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is Attribute1); + } + + [Fact] + public void Map_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is MethodInfo); + Assert.Contains(metadata, m => m is Attribute1); + Assert.Contains(metadata, m => m is DefaultMetadataCountMetadata dmcm && dmcm.Count > 0); + } + + [Fact] + public void Map_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1, Attribute2] () => new RemovesCustomAttributeMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.DoesNotContain(metadata, m => m is Attribute1); + Assert.DoesNotContain(metadata, m => m is Attribute2); + } + + [Fact] + public void Map_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1, Attribute2] (RemovesCustomAttributeParameterMetadataBindable param1) => "Hello"; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.DoesNotContain(metadata, m => m is Attribute1); + Assert.DoesNotContain(metadata, m => m is Attribute2); + } + + [Fact] + public void Map_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1, Attribute2] (RemovesCustomAttributeMetadataBindable param1) => "Hello"; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.DoesNotContain(metadata, m => m is Attribute1); + Assert.DoesNotContain(metadata, m => m is Attribute2); + } + + // TODO: Add tests for: + // - Ordering of calls + // - Removing metadata + // - Accessing default metadata + [Attribute1] [Attribute2] private static Task Handle(HttpContext context) => Task.CompletedTask; @@ -247,4 +421,139 @@ private class Attribute1 : Attribute private class Attribute2 : Attribute { } + + 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 AddsNoEndpointMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + var defaultMetadataCount = context.EndpointMetadata.Count; + context.EndpointMetadata.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount }); + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class RemovesCustomAttributeParameterMetadataBindable : IEndpointParameterMetadataProvider + { + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + { + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is Attribute) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } + } + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class RemovesCustomAttributeMetadataBindable : IEndpointMetadataProvider + { + public static void PopulateMetadata(EndpointMetadataContext parameterContext) + { + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is Attribute) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } + } + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class RemovesCustomAttributeMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = context.EndpointMetadata[i]; + if (metadata is Attribute) + { + context.EndpointMetadata.RemoveAt(i); + } + } + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + 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 AddsNoCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + { + + } + + public static void PopulateMetadata(EndpointMetadataContext context) + { + + } + } + + private class DefaultMetadataCountMetadata + { + public int Count { get; init; } + } + + private class ParameterNameMetadata + { + public string? Name { get; init; } + } + + private class CustomEndpointMetadata + { + public string? Data { get; init; } + + public MetadataSource Source { get; init; } + } + + private enum MetadataSource + { + Parameter, + ReturnType + } } From 532799093ae1284aefbbb2f6c448dfb00005f7e1 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 19:14:38 -0700 Subject: [PATCH 10/20] API review feedback --- .../Builder/EndpointRouteBuilderExtensions.cs | 21 +---- .../Routing/src/EndpointMetadataContext.cs | 9 ++- .../src/EndpointParameterMetadataContext.cs | 28 +++++-- src/Http/Routing/src/PublicAPI.Unshipped.txt | 3 +- ...egateEndpointRouteBuilderExtensionsTest.cs | 79 +++++++++++++++++-- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 7e6dc1af5121..9a923b058b15 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -569,12 +569,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointParameterMetadataProvider - parameterContext ??= new EndpointParameterMetadataContext - { - EndpointMetadata = metadata, - Parameter = parameter, - Services = services - }; + parameterContext ??= new EndpointParameterMetadataContext(parameter, services, metadata); parameterContext.Parameter = parameter; invokeArgs ??= new object[1]; invokeArgs[0] = parameterContext; @@ -584,12 +579,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext - { - EndpointMetadata = metadata, - Method = methodInfo, - Services = services - }; + context ??= new EndpointMetadataContext(methodInfo, services, metadata); invokeArgs ??= new object[1]; invokeArgs[0] = context; PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); @@ -600,12 +590,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) { // Return type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext - { - EndpointMetadata = metadata, - Method = methodInfo, - Services = services - }; + context ??= new EndpointMetadataContext(methodInfo, services, metadata); invokeArgs ??= new object[1]; invokeArgs[0] = context; PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, invokeArgs); diff --git a/src/Http/Routing/src/EndpointMetadataContext.cs b/src/Http/Routing/src/EndpointMetadataContext.cs index 68fc787825be..efb2de951939 100644 --- a/src/Http/Routing/src/EndpointMetadataContext.cs +++ b/src/Http/Routing/src/EndpointMetadataContext.cs @@ -14,10 +14,13 @@ public sealed class EndpointMetadataContext /// Creates a new instance of the . /// /// The associated with the current route handler. - /// The instance used to access application services - /// The objects that will be added to the endpoint's . + /// The instance used to access application services. + /// The objects that will be added to the metadata of the endpoint. public EndpointMetadataContext(MethodInfo method, IServiceProvider? services, IList endpointMetadata) { + ArgumentNullException.ThrowIfNull(method, nameof(method)); + ArgumentNullException.ThrowIfNull(endpointMetadata, nameof(endpointMetadata)); + Method = method; Services = services; EndpointMetadata = endpointMetadata; @@ -34,7 +37,7 @@ public EndpointMetadataContext(MethodInfo method, IServiceProvider? services, IL public IServiceProvider? Services { get; } /// - /// Gets the objects that will be added to the endpoint's . + /// Gets the objects that will be added to the metadata of the endpoint. /// public IList EndpointMetadata { get; } } diff --git a/src/Http/Routing/src/EndpointParameterMetadataContext.cs b/src/Http/Routing/src/EndpointParameterMetadataContext.cs index cd462044f0c5..36b8fe45b0f2 100644 --- a/src/Http/Routing/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Routing/src/EndpointParameterMetadataContext.cs @@ -6,22 +6,38 @@ namespace Microsoft.AspNetCore.Http; /// -/// +/// Represents the information accessible during endpoint creation by types that implement . /// public class EndpointParameterMetadataContext { /// - /// + /// Creates a new instance of the . + /// + /// The parameter of the route handler delegate of the endpoint being created. + /// The instance used to access application services. + /// The objects that will be added to the metadata of the endpoint. + public EndpointParameterMetadataContext(ParameterInfo parameter, IServiceProvider? services, IList endpointMetadata) + { + ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)); + ArgumentNullException.ThrowIfNull(endpointMetadata, nameof(endpointMetadata)); + + Parameter = parameter; + Services = services; + EndpointMetadata = endpointMetadata; + } + + /// + /// Gets the parameter of the route handler delegate of the endpoint being created. /// public ParameterInfo Parameter { get; internal set; } // internal set to allow re-use /// - /// + /// Gets the associated with the current route handler. /// - public IServiceProvider? Services { get; init; } + public IServiceProvider? Services { get; } /// - /// + /// Gets the objects that will be added to the metadata of the endpoint. /// - public IList EndpointMetadata { get; init; } + public IList EndpointMetadata { get; } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index a30153fa8e23..7f2bd53ed909 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -7,10 +7,9 @@ Microsoft.AspNetCore.Http.EndpointMetadataContext.Services.get -> System.IServic Microsoft.AspNetCore.Http.EndpointParameterMetadataContext Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.init -> void -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext(System.Reflection.ParameterInfo! parameter, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.init -> void Microsoft.AspNetCore.Http.IEndpointMetadataProvider Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index ed1007377b7a..a0e1c890bce0 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -333,6 +333,46 @@ public void Map_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIE Assert.Contains(metadata, m => m is DefaultMetadataCountMetadata dmcm && dmcm.Count > 0); } + [Fact] + public void Map_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1] (AddsCustomParameterMetadata param1) => "Hello"; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is MethodInfo); + Assert.Contains(metadata, m => m is Attribute1); + Assert.Contains(metadata, m => m is ParameterNameMetadata pnm); + } + + [Fact] + public void Map_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1] (AddsCustomParameterMetadata param1) => "Hello"; + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Contains(metadata, m => m is MethodInfo); + Assert.Contains(metadata, m => m is Attribute1); + Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); + } + [Fact] public void Map_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() { @@ -390,10 +430,37 @@ public void Map_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndp Assert.DoesNotContain(metadata, m => m is Attribute2); } - // TODO: Add tests for: - // - Ordering of calls - // - Removing metadata - // - Accessing default metadata + [Fact] + public void Map_CallsPopulateMetadata_InCorrectOrder() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => new AddsCustomEndpointMetadataResult(); + + // Act + builder.Map("/test", @delegate); + + // Assert + var ds = GetBuilderEndpointDataSource(builder); + var endpoint = Assert.Single(ds.Endpoints); + var metadata = endpoint.Metadata; + + Assert.Collection(metadata, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => + { + Assert.IsAssignableFrom(m); + Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source); + }, + m => + { + Assert.IsAssignableFrom(m); + Assert.Equal(MetadataSource.ReturnType, ((CustomEndpointMetadata)m).Source); + }); + } [Attribute1] [Attribute2] @@ -541,12 +608,12 @@ private class DefaultMetadataCountMetadata private class ParameterNameMetadata { - public string? Name { get; init; } + public string Name { get; init; } } private class CustomEndpointMetadata { - public string? Data { get; init; } + public string Data { get; init; } public MetadataSource Source { get; init; } } From 1dfa5e0d54dc8fd8513fae6d4f03038c4e3c4438 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 19:19:46 -0700 Subject: [PATCH 11/20] Remove unneeded property --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index c40202a2a8ea..c32a6a790147 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1669,7 +1669,6 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex private class FactoryContext { // Options - public IServiceProvider? ServiceProvider { get; init; } public IServiceProviderIsService? ServiceProviderIsService { get; init; } public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } From 0b516ba009675a0839a760818c8085c2fbf76fec Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 19:23:19 -0700 Subject: [PATCH 12/20] Doc comment fixup --- src/Http/Routing/src/IEndpointMetadataProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Routing/src/IEndpointMetadataProvider.cs b/src/Http/Routing/src/IEndpointMetadataProvider.cs index 69e7df46d953..a315dc1d2655 100644 --- a/src/Http/Routing/src/IEndpointMetadataProvider.cs +++ b/src/Http/Routing/src/IEndpointMetadataProvider.cs @@ -4,13 +4,13 @@ namespace Microsoft.AspNetCore.Http; /// -/// Indicates that a type provides a static method that returns metadata when declared as a parameter type, -/// type, or the returned type of an route handler delegate. +/// 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 { /// - /// Supplies objects to apply as metadata to the related . + /// Populates metadata for the related . /// /// The . static abstract void PopulateMetadata(EndpointMetadataContext context); From e0d37786283acf53eeae4520aa6c117540eb178e Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 5 Apr 2022 10:33:58 -0700 Subject: [PATCH 13/20] Move processing of IProvideEndpointMetadata to RequestDelegateFactory --- .../src/EndpointMetadataContext.cs | 0 .../src/EndpointParameterMetadataContext.cs | 0 .../src/IEndpointMetadataProvider.cs | 0 .../src/IEndpointParameterMetadataProvider.cs | 0 .../src/PublicAPI.Unshipped.txt | 16 + .../src/RequestDelegateFactory.cs | 74 ++++- .../src/RequestDelegateFactoryOptions.cs | 12 +- .../test/RequestDelegateFactoryTests.cs | 307 ++++++++++++++++++ .../Builder/EndpointRouteBuilderExtensions.cs | 100 ++---- src/Http/Routing/src/PublicAPI.Unshipped.txt | 15 - ...egateEndpointRouteBuilderExtensionsTest.cs | 302 +---------------- 11 files changed, 435 insertions(+), 391 deletions(-) rename src/Http/{Routing => Http.Extensions}/src/EndpointMetadataContext.cs (100%) rename src/Http/{Routing => Http.Extensions}/src/EndpointParameterMetadataContext.cs (100%) rename src/Http/{Routing => Http.Extensions}/src/IEndpointMetadataProvider.cs (100%) rename src/Http/{Routing => Http.Extensions}/src/IEndpointParameterMetadataProvider.cs (100%) diff --git a/src/Http/Routing/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs similarity index 100% rename from src/Http/Routing/src/EndpointMetadataContext.cs rename to src/Http/Http.Extensions/src/EndpointMetadataContext.cs diff --git a/src/Http/Routing/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs similarity index 100% rename from src/Http/Routing/src/EndpointParameterMetadataContext.cs rename to src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs diff --git a/src/Http/Routing/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs similarity index 100% rename from src/Http/Routing/src/IEndpointMetadataProvider.cs rename to src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs diff --git a/src/Http/Routing/src/IEndpointParameterMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs similarity index 100% rename from src/Http/Routing/src/IEndpointParameterMetadataProvider.cs rename to src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index f5825c4e8476..a12179619cd3 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,20 @@ #nullable enable +Microsoft.AspNetCore.Http.EndpointMetadataContext +Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void +Microsoft.AspNetCore.Http.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! +Microsoft.AspNetCore.Http.EndpointMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext(System.Reflection.ParameterInfo! parameter, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! +Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.IEndpointMetadataProvider +Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider +Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.DefaultEndpointMetadata.get -> System.Collections.Generic.IReadOnlyList? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.DefaultEndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index c32a6a790147..b547160f4436 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -40,6 +40,8 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!; private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), 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)!; // Call WriteAsJsonAsync() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism @@ -162,11 +164,13 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func new() { + ServiceProvider = options?.ServiceProvider, ServiceProviderIsService = options?.ServiceProvider?.GetService(), RouteParameters = options?.RouteParameterNames?.ToList(), ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, - Filters = options?.RouteHandlerFilterFactories?.ToList() + Filters = options?.RouteHandlerFilterFactories?.ToList(), + EndpointMetadata = options?.DefaultEndpointMetadata }; private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) @@ -187,10 +191,23 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions // 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 caller + if (factoryContext.EndpointMetadata is { Count: > 0 }) + { + foreach (var m in factoryContext.EndpointMetadata) + { + factoryContext.Metadata.Add(m); + } + } + + // Add metadata provided by the delegate return type and parameter types + AddTypeProvidedMetadata(methodInfo, factoryContext.Metadata, factoryContext.ServiceProvider); + // 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.Filters is { Count: > 0 }) @@ -255,6 +272,59 @@ target is null return filteredInvocation; } + private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList metadata, IServiceProvider? services) + { + EndpointParameterMetadataContext? parameterContext = null; + EndpointMetadataContext? context = null; + object?[]? invokeArgs = null; + + // Get metadata from parameter types + var parameters = methodInfo.GetParameters(); + foreach (var parameter in parameters) + { + if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointParameterMetadataProvider + parameterContext ??= new EndpointParameterMetadataContext(parameter, services, metadata); + parameterContext.Parameter = parameter; + invokeArgs ??= new object[1]; + invokeArgs[0] = parameterContext; + PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + + if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) + { + // Parameter type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext(methodInfo, services, metadata); + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); + } + } + + // Get metadata from return type + if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) + { + // Return type implements IEndpointMetadataProvider + context ??= new EndpointMetadataContext(methodInfo, services, metadata); + invokeArgs ??= new object[1]; + invokeArgs[0] = context; + PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.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) { if (parameters is null || parameters.Length == 0) @@ -1669,10 +1739,12 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex private class FactoryContext { // Options + 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; } + public IReadOnlyList? EndpointMetadata { get; init; } // Temporary State public ParameterInfo? JsonRequestBodyParameter { get; set; } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 70207f9c63d8..997a2f4156a1 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http; public sealed class RequestDelegateFactoryOptions { /// - /// The instance used to detect if handler parameters are services. + /// The instance used to access application services. /// public IServiceProvider? ServiceProvider { get; init; } @@ -36,4 +36,14 @@ public sealed class RequestDelegateFactoryOptions /// The list of filters that must run in the pipeline for a given route handler. /// public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } + + /// + /// The default endpoint metadata to add as part of the creation of the . + /// + /// + /// This metadata will be included in after any metadata inferred during creation of the + /// but before any metadata provided by types in the delegate signature that implement + /// or . + /// + public IReadOnlyList? DefaultEndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index f597d15edc15..59692bf9ed38 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4560,6 +4560,169 @@ string HelloName(string name) Assert.Equal("HELLO, TESTNAMEPREFIX!", responseBody); } + [Fact] + public void Create_DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var @delegate = (AddsCustomParameterMetadata param1, AddsCustomParameterMetadata param2) => { }; + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); + Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param2" }); + } + + [Fact] + public void Create_DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = (AddsCustomParameterMetadata param1) => { }; + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); + } + + [Fact] + public void Create_DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = () => new AddsCustomEndpointMetadataResult(); + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); + } + + [Fact] + public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = () => new CountsDefaultEndpointMetadataResult(); + var options = new RequestDelegateFactoryOptions + { + DefaultEndpointMetadata = new List + { + new Attribute1(), + new Attribute2() + } + }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Collection(result.EndpointMetadata, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => + { + Assert.IsAssignableFrom(m); + Assert.Equal(options.DefaultEndpointMetadata.Count, ((DefaultMetadataCountMetadata)m).Count); + }); + } + + [Fact] + public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; + var options = new RequestDelegateFactoryOptions + { + DefaultEndpointMetadata = new List + { + new CustomEndpointMetadata { Source = MetadataSource.Default }, + new Attribute1(), + new Attribute2() + } + }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Collection(result.EndpointMetadata, + m => + { + Assert.IsAssignableFrom(m); + Assert.Equal(MetadataSource.Default, ((CustomEndpointMetadata)m).Source); + }, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), + m => + { + Assert.IsAssignableFrom(m); + Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source); + }); + } + + [Fact] + public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; + var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1() } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is Attribute1); + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); + } + + [Fact] + public void Create_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = [Attribute1, Attribute2] () => new RemovesCustomAttributeMetadataResult(); + var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + } + + [Fact] + public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() + { + // Arrange + var @delegate = (RemovesCustomAttributeParameterMetadataBindable param1) => "Hello"; + var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + } + + [Fact] + public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = (RemovesCustomAttributeMetadataBindable param1) => "Hello"; + var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); + Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + } + private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); @@ -4576,6 +4739,150 @@ private DefaultHttpContext CreateHttpContext() }; } + private class Attribute1 : Attribute + { + } + + private class Attribute2 : Attribute + { + } + + 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 AddsNoEndpointMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + var defaultMetadataCount = context.EndpointMetadata.Count; + context.EndpointMetadata.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount }); + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class RemovesCustomAttributeParameterMetadataBindable : IEndpointParameterMetadataProvider + { + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + { + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is Attribute) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } + } + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class RemovesCustomAttributeMetadataBindable : IEndpointMetadataProvider + { + public static void PopulateMetadata(EndpointMetadataContext parameterContext) + { + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is Attribute) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } + } + } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + } + + private class RemovesCustomAttributeMetadataResult : IEndpointMetadataProvider, IResult + { + public static void PopulateMetadata(EndpointMetadataContext context) + { + for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) + { + var metadata = context.EndpointMetadata[i]; + if (metadata is Attribute) + { + context.EndpointMetadata.RemoveAt(i); + } + } + } + + public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); + } + + private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + 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 AddsNoCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; + + public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) + { + + } + + public static void PopulateMetadata(EndpointMetadataContext context) + { + + } + } + + private class DefaultMetadataCountMetadata + { + public int Count { get; init; } + } + + private class ParameterNameMetadata + { + public string? Name { get; init; } + } + + private class CustomEndpointMetadata + { + public string? Data { get; init; } + + public MetadataSource Source { get; init; } + } + + private enum MetadataSource + { + Default, + Parameter, + ReturnType + } + private class Todo : ITodo { public int Id { get; set; } diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 9a923b058b15..42c073177ba3 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -24,8 +24,6 @@ public static class EndpointRouteBuilderExtensions private static readonly string[] PutVerb = new[] { HttpMethods.Put }; private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; - private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointRouteBuilderExtensions).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointRouteBuilderExtensions).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; /// /// Adds a to the that matches HTTP GET requests @@ -459,7 +457,8 @@ private static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, Delegate handler, - bool disableInferBodyFromParameters) + bool disableInferBodyFromParameters, + IReadOnlyList? endpointMetadata = null) { if (endpoints is null) { @@ -478,6 +477,8 @@ private static RouteHandlerBuilder Map( const int defaultOrder = 0; + var metadataToAdd = new List(); + var routeParams = new List(pattern.Parameters.Count); foreach (var part in pattern.Parameters) { @@ -497,8 +498,28 @@ private static RouteHandlerBuilder Map( // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. + // This is added directly to the builder to ensure it's first in the metadata collection. builder.Metadata.Add(handler.Method); + // Defer adding attributes on the handler to the call to RDF as those automatically generated by the + // RDF have a higher specificity. + var attributes = handler.Method.GetCustomAttributes(); + + // This can be null if the delegate is a dynamic method or compiled from an expression tree + if (attributes is not null) + { + foreach (var attribute in attributes) + { + metadataToAdd.Add(attribute); + } + } + + // Add additional metadata provided by caller + if (endpointMetadata is not null) + { + metadataToAdd.AddRange(endpointMetadata); + } + // Methods defined in a top-level program are generated as statics so the delegate // target will be null. Inline lambdas are compiler generated method so they can // be filtered that way. @@ -525,87 +546,20 @@ private static RouteHandlerBuilder Map( RouteParameterNames = routeParams, ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, DisableInferBodyFromParameters = disableInferBodyFromParameters, - RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories + RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories, + DefaultEndpointMetadata = metadataToAdd }; var filteredRequestDelegateResult = RequestDelegateFactory.Create(handler, options); + // Add request delegate metadata foreach (var metadata in filteredRequestDelegateResult.EndpointMetadata) { endpointBuilder.Metadata.Add(metadata); } - // We add attributes on the handler after those automatically generated by the - // RDF since they have a higher specificity. - var attributes = handler.Method.GetCustomAttributes(); - - // This can be null if the delegate is a dynamic method or compiled from an expression tree - if (attributes is not null) - { - foreach (var attribute in attributes) - { - endpointBuilder.Metadata.Add(attribute); - } - } - - // Add metadata provided by the delegate return type and parameter types - AddTypeProvidedMetadata(handler.Method, endpointBuilder.Metadata, endpoints.ServiceProvider); - endpointBuilder.RequestDelegate = filteredRequestDelegateResult.RequestDelegate; }); return routeHandlerBuilder; } - - private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList metadata, IServiceProvider? services) - { - EndpointParameterMetadataContext? parameterContext = null; - EndpointMetadataContext? context = null; - object?[]? invokeArgs = null; - - // Get metadata from parameter types - var parameters = methodInfo.GetParameters(); - foreach (var parameter in parameters) - { - if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointParameterMetadataProvider - parameterContext ??= new EndpointParameterMetadataContext(parameter, services, metadata); - parameterContext.Parameter = parameter; - invokeArgs ??= new object[1]; - invokeArgs[0] = parameterContext; - PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - - if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) - { - // Parameter type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext(methodInfo, services, metadata); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); - } - } - - // Get metadata from return type - if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) - { - // Return type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext(methodInfo, services, metadata); - invokeArgs ??= new object[1]; - invokeArgs[0] = context; - PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.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); - } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 7f2bd53ed909..432fe7abd574 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -1,19 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Http.EndpointMetadataContext -Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void -Microsoft.AspNetCore.Http.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! -Microsoft.AspNetCore.Http.EndpointMetadataContext.Services.get -> System.IServiceProvider? -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.init -> void -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext(System.Reflection.ParameterInfo! parameter, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? -Microsoft.AspNetCore.Http.IEndpointMetadataProvider -Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token, System.Type! type) -> void Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token) -> void diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index a0e1c890bce0..c56576c3c666 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -223,215 +223,7 @@ public void AddingMetadataAfterBuildingEndpointThrows(Func()); - var @delegate = (AddsCustomParameterMetadata param1, AddsCustomParameterMetadata param2) => { }; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param1", StringComparison.Ordinal)); - Assert.Contains(metadata, m => m is ParameterNameMetadata pnm && string.Equals(pnm.Name, "param2", StringComparison.Ordinal)); - } - - [Fact] - public void Map_DiscoversMetadata_FromParametersImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = (AddsCustomParameterMetadata param1) => { }; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); - } - - [Fact] - public void Map_DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = () => new AddsCustomEndpointMetadataResult(); - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.ReturnType); - } - - [Fact] - public void Map_ProvidesDefaultMethodInfoMetadata_ToReturnTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is MethodInfo); - } - - [Fact] - public void Map_ProvidesDefaultMethodAttributeMetadata_ToReturnTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is Attribute1); - } - - [Fact] - public void Map_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1] () => new CountsDefaultEndpointMetadataResult(); - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is MethodInfo); - Assert.Contains(metadata, m => m is Attribute1); - Assert.Contains(metadata, m => m is DefaultMetadataCountMetadata dmcm && dmcm.Count > 0); - } - - [Fact] - public void Map_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1] (AddsCustomParameterMetadata param1) => "Hello"; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is MethodInfo); - Assert.Contains(metadata, m => m is Attribute1); - Assert.Contains(metadata, m => m is ParameterNameMetadata pnm); - } - - [Fact] - public void Map_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1] (AddsCustomParameterMetadata param1) => "Hello"; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.Contains(metadata, m => m is MethodInfo); - Assert.Contains(metadata, m => m is Attribute1); - Assert.Contains(metadata, m => m is CustomEndpointMetadata cem && cem.Source == MetadataSource.Parameter); - } - - [Fact] - public void Map_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1, Attribute2] () => new RemovesCustomAttributeMetadataResult(); - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.DoesNotContain(metadata, m => m is Attribute1); - Assert.DoesNotContain(metadata, m => m is Attribute2); - } - - [Fact] - public void Map_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1, Attribute2] (RemovesCustomAttributeParameterMetadataBindable param1) => "Hello"; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.DoesNotContain(metadata, m => m is Attribute1); - Assert.DoesNotContain(metadata, m => m is Attribute2); - } - - [Fact] - public void Map_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - var @delegate = [Attribute1, Attribute2] (RemovesCustomAttributeMetadataBindable param1) => "Hello"; - - // Act - builder.Map("/test", @delegate); - - // Assert - var ds = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(ds.Endpoints); - var metadata = endpoint.Metadata; - - Assert.DoesNotContain(metadata, m => m is Attribute1); - Assert.DoesNotContain(metadata, m => m is Attribute2); - } - - [Fact] - public void Map_CallsPopulateMetadata_InCorrectOrder() + public void Map_AddsMetadata_InCorrectOrder() { // Arrange var builder = new DefaultEndpointRouteBuilder(Mock.Of()); @@ -499,78 +291,6 @@ public static void PopulateMetadata(EndpointMetadataContext context) public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } - private class AddsNoEndpointMetadataResult : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - - private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - var defaultMetadataCount = context.EndpointMetadata.Count; - context.EndpointMetadata.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount }); - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - - private class RemovesCustomAttributeParameterMetadataBindable : IEndpointParameterMetadataProvider - { - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) - { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = parameterContext.EndpointMetadata[i]; - if (metadata is Attribute) - { - parameterContext.EndpointMetadata.RemoveAt(i); - } - } - } - - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - } - - private class RemovesCustomAttributeMetadataBindable : IEndpointMetadataProvider - { - public static void PopulateMetadata(EndpointMetadataContext parameterContext) - { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = parameterContext.EndpointMetadata[i]; - if (metadata is Attribute) - { - parameterContext.EndpointMetadata.RemoveAt(i); - } - } - } - - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - } - - private class RemovesCustomAttributeMetadataResult : IEndpointMetadataProvider, IResult - { - public static void PopulateMetadata(EndpointMetadataContext context) - { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) - { - var metadata = context.EndpointMetadata[i]; - if (metadata is Attribute) - { - context.EndpointMetadata.RemoveAt(i); - } - } - } - - public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); - } - private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; @@ -586,26 +306,6 @@ public static void PopulateMetadata(EndpointMetadataContext context) } } - private class AddsNoCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) - { - - } - - public static void PopulateMetadata(EndpointMetadataContext context) - { - - } - } - - private class DefaultMetadataCountMetadata - { - public int Count { get; init; } - } - private class ParameterNameMetadata { public string Name { get; init; } From 18ee92aa69b9312aa77f5a3776f50220548c273d Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 5 Apr 2022 16:43:10 -0700 Subject: [PATCH 14/20] PR feedback --- .../src/EndpointMetadataContext.cs | 24 +--- .../src/EndpointParameterMetadataContext.cs | 24 +--- .../src/IEndpointMetadataProvider.cs | 2 +- .../src/IEndpointParameterMetadataProvider.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 37 +++--- .../src/RequestDelegateFactory.cs | 44 ++++--- .../src/RequestDelegateFactoryOptions.cs | 11 +- .../test/RequestDelegateFactoryTests.cs | 109 +++++++++--------- .../Builder/EndpointRouteBuilderExtensions.cs | 32 +++-- ...egateEndpointRouteBuilderExtensionsTest.cs | 7 +- 10 files changed, 140 insertions(+), 152 deletions(-) diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs index efb2de951939..c7ac2e457f09 100644 --- a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs @@ -3,41 +3,25 @@ using System.Reflection; -namespace Microsoft.AspNetCore.Http; +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 . - /// - /// The associated with the current route handler. - /// The instance used to access application services. - /// The objects that will be added to the metadata of the endpoint. - public EndpointMetadataContext(MethodInfo method, IServiceProvider? services, IList endpointMetadata) - { - ArgumentNullException.ThrowIfNull(method, nameof(method)); - ArgumentNullException.ThrowIfNull(endpointMetadata, nameof(endpointMetadata)); - - Method = method; - Services = services; - EndpointMetadata = endpointMetadata; - } - /// /// Gets the associated with the current route handler. /// - public MethodInfo Method { get; } + public MethodInfo? Method { get; init; } /// /// Gets the instance used to access application services. /// - public IServiceProvider? Services { get; } + public IServiceProvider? Services { get; init; } /// /// Gets the objects that will be added to the metadata of the endpoint. /// - public IList EndpointMetadata { get; } + public IList? EndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index 36b8fe45b0f2..dc44359c0072 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -3,41 +3,25 @@ using System.Reflection; -namespace Microsoft.AspNetCore.Http; +namespace Microsoft.AspNetCore.Http.Metadata; /// /// Represents the information accessible during endpoint creation by types that implement . /// public class EndpointParameterMetadataContext { - /// - /// Creates a new instance of the . - /// - /// The parameter of the route handler delegate of the endpoint being created. - /// The instance used to access application services. - /// The objects that will be added to the metadata of the endpoint. - public EndpointParameterMetadataContext(ParameterInfo parameter, IServiceProvider? services, IList endpointMetadata) - { - ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)); - ArgumentNullException.ThrowIfNull(endpointMetadata, nameof(endpointMetadata)); - - Parameter = parameter; - Services = services; - EndpointMetadata = endpointMetadata; - } - /// /// Gets the parameter of the route handler delegate of the endpoint being created. /// - public ParameterInfo Parameter { get; internal set; } // internal set to allow re-use + public ParameterInfo? Parameter { get; internal set; } // internal set to allow re-use /// /// Gets the associated with the current route handler. /// - public IServiceProvider? Services { get; } + public IServiceProvider? Services { get; init; } /// /// Gets the objects that will be added to the metadata of the endpoint. /// - public IList EndpointMetadata { get; } + public IList? EndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs index a315dc1d2655..7ffdcc4c49d9 100644 --- a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs +++ b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs @@ -1,7 +1,7 @@ // 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; +namespace Microsoft.AspNetCore.Http.Metadata; /// /// Indicates that a type provides a static method that provides metadata when declared as a parameter type or the diff --git a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs index 2968dcb1da34..0c7731154271 100644 --- a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs +++ b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs @@ -1,7 +1,7 @@ // 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; +namespace Microsoft.AspNetCore.Http.Metadata; /// /// Indicates that a type provides a static method that returns metadata when declared as the diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index a12179619cd3..26610653f279 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,20 +1,25 @@ #nullable enable -Microsoft.AspNetCore.Http.EndpointMetadataContext -Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.EndpointMetadataContext.EndpointMetadataContext(System.Reflection.MethodInfo! method, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void -Microsoft.AspNetCore.Http.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! -Microsoft.AspNetCore.Http.EndpointMetadataContext.Services.get -> System.IServiceProvider? -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.EndpointParameterMetadataContext(System.Reflection.ParameterInfo! parameter, System.IServiceProvider? services, System.Collections.Generic.IList! endpointMetadata) -> void -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! -Microsoft.AspNetCore.Http.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? -Microsoft.AspNetCore.Http.IEndpointMetadataProvider -Microsoft.AspNetCore.Http.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointMetadataContext! context) -> void -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider -Microsoft.AspNetCore.Http.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.EndpointParameterMetadataContext! parameterContext) -> void -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.DefaultEndpointMetadata.get -> System.Collections.Generic.IReadOnlyList? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.DefaultEndpointMetadata.init -> void +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList? +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.init -> void +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext() -> void +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo? +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.init -> void +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.init -> void +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.init -> void +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.RequestDelegateFactoryOptions.AdditionalEndpointMetadata.get -> System.Collections.Generic.IEnumerable? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.AdditionalEndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index b547160f4436..e057497dacdd 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -170,7 +170,7 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, Filters = options?.RouteHandlerFilterFactories?.ToList(), - EndpointMetadata = options?.DefaultEndpointMetadata + AdditionalEndpointMetadata = options?.AdditionalEndpointMetadata }; private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) @@ -191,23 +191,23 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions // return default; // } + // Add MethodInfo as first metadata item + factoryContext.Metadata.Insert(0, methodInfo); + // 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 caller - if (factoryContext.EndpointMetadata is { Count: > 0 }) - { - foreach (var m in factoryContext.EndpointMetadata) - { - factoryContext.Metadata.Add(m); - } - } - // Add metadata provided by the delegate return type and parameter types AddTypeProvidedMetadata(methodInfo, factoryContext.Metadata, factoryContext.ServiceProvider); + // Add metadata provided by the caller last so it is the most specific, i.e. can override inferred metadata + if (factoryContext.AdditionalEndpointMetadata is not null) + { + factoryContext.Metadata.AddRange(factoryContext.AdditionalEndpointMetadata); + } + // 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.Filters is { Count: > 0 }) @@ -285,7 +285,11 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointParameterMetadataProvider - parameterContext ??= new EndpointParameterMetadataContext(parameter, services, metadata); + parameterContext ??= new EndpointParameterMetadataContext + { + EndpointMetadata = metadata, + Services = services + }; parameterContext.Parameter = parameter; invokeArgs ??= new object[1]; invokeArgs[0] = parameterContext; @@ -295,7 +299,12 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext(methodInfo, services, metadata); + context ??= new EndpointMetadataContext + { + Method = methodInfo, + EndpointMetadata = metadata, + Services = services + }; invokeArgs ??= new object[1]; invokeArgs[0] = context; PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); @@ -306,7 +315,12 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) { // Return type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext(methodInfo, services, metadata); + context ??= new EndpointMetadataContext + { + Method = methodInfo, + EndpointMetadata = metadata, + Services = services + }; invokeArgs ??= new object[1]; invokeArgs[0] = context; PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, invokeArgs); @@ -1744,7 +1758,7 @@ private class FactoryContext public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } public bool DisableInferredFromBody { get; init; } - public IReadOnlyList? EndpointMetadata { get; init; } + public IEnumerable? AdditionalEndpointMetadata { get; init; } // Temporary State public ParameterInfo? JsonRequestBodyParameter { get; set; } @@ -1759,7 +1773,7 @@ private class FactoryContext public bool HasMultipleBodyParameters { get; set; } public bool HasInferredBody { get; set; } - public List Metadata { get; } = new(); + public List Metadata { get; internal set; } = new(); public NullabilityInfoContext NullabilityContext { get; } = new(); diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 997a2f4156a1..d0e5ebf43fb9 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -38,12 +38,13 @@ public sealed class RequestDelegateFactoryOptions public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } /// - /// The default endpoint metadata to add as part of the creation of the . + /// The additional endpoint metadata to add as part of the creation of the . /// /// - /// This metadata will be included in after any metadata inferred during creation of the - /// but before any metadata provided by types in the delegate signature that implement - /// or . + /// This metadata will be included in after any metadata inferred during creation of the + /// and after any metadata provided by types in the delegate signature that implement + /// or , i.e. this metadata will be more specific than any + /// inferred by the call to . /// - public IReadOnlyList? DefaultEndpointMetadata { get; init; } + public IEnumerable? AdditionalEndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 59692bf9ed38..02e7d50c6826 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4607,7 +4607,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin var @delegate = () => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { - DefaultEndpointMetadata = new List + AdditionalEndpointMetadata = new List { new Attribute1(), new Attribute2() @@ -4619,13 +4619,15 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin // Assert Assert.Collection(result.EndpointMetadata, - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), m => { Assert.IsAssignableFrom(m); - Assert.Equal(options.DefaultEndpointMetadata.Count, ((DefaultMetadataCountMetadata)m).Count); - }); + // Expecting '1' as only the MethodInfo will be in the metadata list when this metadata item is added + Assert.Equal(1, ((DefaultMetadataCountMetadata)m).Count); + }, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m)); } [Fact] @@ -4635,9 +4637,9 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { - DefaultEndpointMetadata = new List + AdditionalEndpointMetadata = new List { - new CustomEndpointMetadata { Source = MetadataSource.Default }, + new CustomEndpointMetadata { Source = MetadataSource.Caller }, new Attribute1(), new Attribute2() } @@ -4648,19 +4650,20 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen // Assert Assert.Collection(result.EndpointMetadata, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m), m => { Assert.IsAssignableFrom(m); - Assert.Equal(MetadataSource.Default, ((CustomEndpointMetadata)m).Source); + Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source); }, - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), m => { Assert.IsAssignableFrom(m); - Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source); - }); + Assert.Equal(MetadataSource.Caller, ((CustomEndpointMetadata)m).Source); + }, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m)); } [Fact] @@ -4668,7 +4671,7 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { // Arrange var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; - var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1() } }; + var options = new RequestDelegateFactoryOptions { AdditionalEndpointMetadata = new List { new Attribute1() } }; // Act var result = RequestDelegateFactory.Create(@delegate, options); @@ -4682,45 +4685,40 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen public void Create_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() { // Arrange - var @delegate = [Attribute1, Attribute2] () => new RemovesCustomAttributeMetadataResult(); - var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + var @delegate = (Todo todo) => new RemovesAcceptsMetadataResult(); // Act - var result = RequestDelegateFactory.Create(@delegate, options); + var result = RequestDelegateFactory.Create(@delegate); // Assert - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() { // Arrange - var @delegate = (RemovesCustomAttributeParameterMetadataBindable param1) => "Hello"; - var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello"; // Act - var result = RequestDelegateFactory.Create(@delegate, options); + var result = RequestDelegateFactory.Create(@delegate); // Assert - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider() { // Arrange - var @delegate = (RemovesCustomAttributeMetadataBindable param1) => "Hello"; - var options = new RequestDelegateFactoryOptions { DefaultEndpointMetadata = new List { new Attribute1(), new Attribute2() } }; + var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello"; + var options = new RequestDelegateFactoryOptions(); // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute1); - Assert.DoesNotContain(result.EndpointMetadata, m => m is Attribute2); + Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } private DefaultHttpContext CreateHttpContext() @@ -4751,7 +4749,7 @@ private class AddsCustomEndpointMetadataResult : IEndpointMetadataProvider, IRes { public static void PopulateMetadata(EndpointMetadataContext context) { - context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); + context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.ReturnType }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); @@ -4771,57 +4769,62 @@ private class CountsDefaultEndpointMetadataResult : IEndpointMetadataProvider, I { public static void PopulateMetadata(EndpointMetadataContext context) { - var defaultMetadataCount = context.EndpointMetadata.Count; - context.EndpointMetadata.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount }); + var defaultMetadataCount = context.EndpointMetadata?.Count; + context.EndpointMetadata?.Add(new DefaultMetadataCountMetadata { Count = defaultMetadataCount ?? 0 }); } public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException(); } - private class RemovesCustomAttributeParameterMetadataBindable : IEndpointParameterMetadataProvider + private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider { public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + if (parameterContext.EndpointMetadata is not null) { - var metadata = parameterContext.EndpointMetadata[i]; - if (metadata is Attribute) + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) { - parameterContext.EndpointMetadata.RemoveAt(i); + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is IAcceptsMetadata) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } } } } - - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; } - private class RemovesCustomAttributeMetadataBindable : IEndpointMetadataProvider + private class RemovesAcceptsMetadata : IEndpointMetadataProvider { public static void PopulateMetadata(EndpointMetadataContext parameterContext) { - for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) + if (parameterContext.EndpointMetadata is not null) { - var metadata = parameterContext.EndpointMetadata[i]; - if (metadata is Attribute) + for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--) { - parameterContext.EndpointMetadata.RemoveAt(i); + var metadata = parameterContext.EndpointMetadata[i]; + if (metadata is IAcceptsMetadata) + { + parameterContext.EndpointMetadata.RemoveAt(i); + } } } } - - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; } - private class RemovesCustomAttributeMetadataResult : IEndpointMetadataProvider, IResult + private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult { public static void PopulateMetadata(EndpointMetadataContext context) { - for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) + if (context.EndpointMetadata is not null) { - var metadata = context.EndpointMetadata[i]; - if (metadata is Attribute) + for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--) { - context.EndpointMetadata.RemoveAt(i); + var metadata = context.EndpointMetadata[i]; + if (metadata is IAcceptsMetadata) + { + context.EndpointMetadata.RemoveAt(i); + } } } } @@ -4835,12 +4838,12 @@ private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) { - parameterContext.EndpointMetadata.Add(new ParameterNameMetadata { Name = parameterContext.Parameter.Name }); + parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); } public static void PopulateMetadata(EndpointMetadataContext context) { - context.EndpointMetadata.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); + context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } } @@ -4878,7 +4881,7 @@ private class CustomEndpointMetadata private enum MetadataSource { - Default, + Caller, Parameter, ReturnType } diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 42c073177ba3..cf2922db42cd 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -322,10 +322,12 @@ public static RouteHandlerBuilder MapMethods( } } - var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody); + var initialMetadata = new object[] { new HttpMethodMetadata(httpMethods) }; + var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody, initialMetadata); + // Prepends the HTTP method to the DisplayName produced with pattern + method name builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}"); - builder.WithMetadata(new HttpMethodMetadata(httpMethods)); + return builder; static bool ShouldDisableInferredBody(string method) @@ -458,7 +460,7 @@ private static RouteHandlerBuilder Map( RoutePattern pattern, Delegate handler, bool disableInferBodyFromParameters, - IReadOnlyList? endpointMetadata = null) + IEnumerable? additionalEndpointMetadata = null) { if (endpoints is null) { @@ -477,7 +479,7 @@ private static RouteHandlerBuilder Map( const int defaultOrder = 0; - var metadataToAdd = new List(); + var additionalMetadata = new List(); var routeParams = new List(pattern.Parameters.Count); foreach (var part in pattern.Parameters) @@ -497,27 +499,21 @@ private static RouteHandlerBuilder Map( // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? - // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. - // This is added directly to the builder to ensure it's first in the metadata collection. - builder.Metadata.Add(handler.Method); - - // Defer adding attributes on the handler to the call to RDF as those automatically generated by the - // RDF have a higher specificity. + // Defer adding handler attributes as metadata to the call to RDF. RDF should add this metadata + // after any inferred metadata so that the additional metadata has a higher specificity. var attributes = handler.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree if (attributes is not null) { - foreach (var attribute in attributes) - { - metadataToAdd.Add(attribute); - } + additionalMetadata.AddRange(attributes); } - // Add additional metadata provided by caller - if (endpointMetadata is not null) + // Add additional metadata provided by caller, deferred to the call to RDF. RDF should add this metadata + // after any inferred metadata so that the additional metadata has a higher specificity. + if (additionalEndpointMetadata is not null) { - metadataToAdd.AddRange(endpointMetadata); + additionalMetadata.AddRange(additionalEndpointMetadata); } // Methods defined in a top-level program are generated as statics so the delegate @@ -547,7 +543,7 @@ private static RouteHandlerBuilder Map( ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, DisableInferBodyFromParameters = disableInferBodyFromParameters, RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories, - DefaultEndpointMetadata = metadataToAdd + AdditionalEndpointMetadata = additionalMetadata }; var filteredRequestDelegateResult = RequestDelegateFactory.Create(handler, options); diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index c56576c3c666..54f4b313ecc1 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -7,6 +7,7 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; @@ -239,8 +240,6 @@ public void Map_AddsMetadata_InCorrectOrder() Assert.Collection(metadata, m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), m => Assert.IsAssignableFrom(m), m => { @@ -251,7 +250,9 @@ public void Map_AddsMetadata_InCorrectOrder() { Assert.IsAssignableFrom(m); Assert.Equal(MetadataSource.ReturnType, ((CustomEndpointMetadata)m).Source); - }); + }, + m => Assert.IsAssignableFrom(m), + m => Assert.IsAssignableFrom(m)); } [Attribute1] From d0fed853e53c40e2107777da80a0276d1c5c40a1 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 5 Apr 2022 17:13:08 -0700 Subject: [PATCH 15/20] Fix setter --- .../Http.Extensions/src/EndpointParameterMetadataContext.cs | 2 +- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index dc44359c0072..d88286b43eb1 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -13,7 +13,7 @@ public class EndpointParameterMetadataContext /// /// Gets the parameter of the route handler delegate of the endpoint being created. /// - public ParameterInfo? Parameter { get; internal set; } // internal set to allow re-use + public ParameterInfo? Parameter { get; set; } // set to allow re-use /// /// Gets the associated with the current route handler. diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 26610653f279..c2b1e254d3fd 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -12,6 +12,7 @@ Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMeta Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.set -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.init -> void Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider From 66cc5003b8c117f0bd4e55f99dea60e485bed2d8 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 09:38:49 -0700 Subject: [PATCH 16/20] Addressing PR feedback --- .../src/EndpointParameterMetadataContext.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 6 +- .../src/RequestDelegateFactory.cs | 49 ++++-- .../src/RequestDelegateFactoryOptions.cs | 10 +- .../test/RequestDelegateFactoryTests.cs | 157 +++++++++++++----- .../Builder/EndpointRouteBuilderExtensions.cs | 33 ++-- ...ndlerEndpointRouteBuilderExtensionsTest.cs | 4 +- 7 files changed, 169 insertions(+), 92 deletions(-) diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index d88286b43eb1..861964e5a9a5 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -13,7 +13,7 @@ public class EndpointParameterMetadataContext /// /// Gets the parameter of the route handler delegate of the endpoint being created. /// - public ParameterInfo? Parameter { get; set; } // set to allow re-use + public ParameterInfo? Parameter { get; init; } /// /// Gets the associated with the current route handler. diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index c2b1e254d3fd..481648080c35 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -12,15 +12,15 @@ Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMeta Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo? -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.set -> void +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.init -> void 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.RequestDelegateFactoryOptions.AdditionalEndpointMetadata.get -> System.Collections.Generic.IEnumerable? -Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.AdditionalEndpointMetadata.init -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata.get -> System.Collections.Generic.IEnumerable? +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList!>? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index e057497dacdd..b7a9b7fd111b 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -161,18 +161,26 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); } - private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) => - new() + private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) + { + var context = new FactoryContext { ServiceProvider = options?.ServiceProvider, ServiceProviderIsService = options?.ServiceProvider?.GetService(), RouteParameters = options?.RouteParameterNames?.ToList(), ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, - Filters = options?.RouteHandlerFilterFactories?.ToList(), - AdditionalEndpointMetadata = options?.AdditionalEndpointMetadata + Filters = options?.RouteHandlerFilterFactories?.ToList() }; + if (options?.InitialEndpointMetadata is not null) + { + context.Metadata.AddRange(options.InitialEndpointMetadata); + } + + return context; + } + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) { // Non void return type @@ -199,14 +207,11 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions var returnType = methodInfo.ReturnType; factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments); - // Add metadata provided by the delegate return type and parameter types + // 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); - // Add metadata provided by the caller last so it is the most specific, i.e. can override inferred metadata - if (factoryContext.AdditionalEndpointMetadata is not null) - { - factoryContext.Metadata.AddRange(factoryContext.AdditionalEndpointMetadata); - } + // Add method attributes as metadata *after* any inferred metadata so that the attributes hava a higher specificity + AddMethodAttributesAsMetadata(methodInfo, factoryContext.Metadata); // 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. @@ -272,10 +277,8 @@ target is null return filteredInvocation; } - private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList metadata, IServiceProvider? services) + private static void AddTypeProvidedMetadata(MethodInfo methodInfo, List metadata, IServiceProvider? services) { - EndpointParameterMetadataContext? parameterContext = null; - EndpointMetadataContext? context = null; object?[]? invokeArgs = null; // Get metadata from parameter types @@ -285,12 +288,12 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointParameterMetadataProvider - parameterContext ??= new EndpointParameterMetadataContext + var parameterContext = new EndpointParameterMetadataContext { + Parameter = parameter, EndpointMetadata = metadata, Services = services }; - parameterContext.Parameter = parameter; invokeArgs ??= new object[1]; invokeArgs[0] = parameterContext; PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs); @@ -299,7 +302,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType)) { // Parameter type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext + var context = new EndpointMetadataContext { Method = methodInfo, EndpointMetadata = metadata, @@ -315,7 +318,7 @@ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType)) { // Return type implements IEndpointMetadataProvider - context ??= new EndpointMetadataContext + var context = new EndpointMetadataContext { Method = methodInfo, EndpointMetadata = metadata, @@ -339,6 +342,17 @@ private static void PopulateMetadataForEndpoint(EndpointMetadataContext conte T.PopulateMetadata(context); } + private static void AddMethodAttributesAsMetadata(MethodInfo methodInfo, List metadata) + { + var attributes = methodInfo.GetCustomAttributes(); + + // This can be null if the delegate is a dynamic method or compiled from an expression tree + if (attributes is not null) + { + metadata.AddRange(attributes); + } + } + private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext) { if (parameters is null || parameters.Length == 0) @@ -1758,7 +1772,6 @@ private class FactoryContext public List? RouteParameters { get; init; } public bool ThrowOnBadRequest { get; init; } public bool DisableInferredFromBody { get; init; } - public IEnumerable? AdditionalEndpointMetadata { get; init; } // Temporary State public ParameterInfo? JsonRequestBodyParameter { get; set; } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index d0e5ebf43fb9..9b367dfcfcf0 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -38,13 +38,13 @@ public sealed class RequestDelegateFactoryOptions public IReadOnlyList>? RouteHandlerFilterFactories { get; init; } /// - /// The additional endpoint metadata to add as part of the creation of the . + /// The initial endpoint metadata to add as part of the creation of the . /// /// - /// This metadata will be included in after any metadata inferred during creation of the - /// and after any metadata provided by types in the delegate signature that implement - /// or , i.e. this metadata will be more specific than any + /// This metadata will be included in before any metadata inferred during creation of the + /// and before any metadata provided by types in the delegate signature that implement + /// or , i.e. this metadata will be less specific than any /// inferred by the call to . /// - public IEnumerable? AdditionalEndpointMetadata { get; init; } + public IEnumerable? InitialEndpointMetadata { get; init; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 02e7d50c6826..5eea1bcf753c 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4560,11 +4560,69 @@ string HelloName(string name) Assert.Equal("HELLO, TESTNAMEPREFIX!", responseBody); } + [Fact] + public void Create_AddsDelegateMethodInfo_AsMetadata() + { + // Arrange + var @delegate = () => "Hello"; + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is MethodInfo); + } + + [Fact] + public void Create_AddsDelegateMethodInfo_AsFirstMetadata() + { + // Arrange + var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; + var customMetadata = new CustomEndpointMetadata(); + var options = new RequestDelegateFactoryOptions { InitialEndpointMetadata = new[] { customMetadata } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + var firstMetadata = result.EndpointMetadata[0]; + Assert.IsAssignableFrom(firstMetadata); + } + + [Fact] + public void Create_AddsDelegateAttributes_AsMetadata() + { + // Arrange + var @delegate = [Attribute1, Attribute2] () => { }; + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is Attribute1); + Assert.Contains(result.EndpointMetadata, m => m is Attribute2); + } + + [Fact] + public void Create_AddsDelegateAttributes_AsLastMetadata() + { + // Arrange + var @delegate = [Attribute1] (AddsCustomParameterMetadata param1) => { }; + var options = new RequestDelegateFactoryOptions { InitialEndpointMetadata = new[] { new CustomEndpointMetadata() } }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + var lastMetadata = result.EndpointMetadata.Last(); + Assert.IsAssignableFrom(lastMetadata); + } + [Fact] public void Create_DiscoversMetadata_FromParametersImplementingIEndpointParameterMetadataProvider() { // Arrange - var @delegate = (AddsCustomParameterMetadata param1, AddsCustomParameterMetadata param2) => { }; + var @delegate = (AddsCustomParameterMetadataBindable param1, AddsCustomParameterMetadata param2) => { }; // Act var result = RequestDelegateFactory.Create(@delegate); @@ -4607,10 +4665,9 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin var @delegate = () => new CountsDefaultEndpointMetadataResult(); var options = new RequestDelegateFactoryOptions { - AdditionalEndpointMetadata = new List + InitialEndpointMetadata = new List { - new Attribute1(), - new Attribute2() + new CustomEndpointMetadata { Source = MetadataSource.Caller } } }; @@ -4618,16 +4675,9 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementin var result = RequestDelegateFactory.Create(@delegate, options); // Assert - Assert.Collection(result.EndpointMetadata, - m => Assert.IsAssignableFrom(m), - m => - { - Assert.IsAssignableFrom(m); - // Expecting '1' as only the MethodInfo will be in the metadata list when this metadata item is added - Assert.Equal(1, ((DefaultMetadataCountMetadata)m).Count); - }, - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m)); + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); + // Expecting '2' as only MethodInfo and initial metadata will be in the metadata list when this metadata item is added + Assert.Contains(result.EndpointMetadata, m => m is DefaultMetadataCountMetadata { Count: 2 }); } [Fact] @@ -4637,11 +4687,9 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; var options = new RequestDelegateFactoryOptions { - AdditionalEndpointMetadata = new List + InitialEndpointMetadata = new List { - new CustomEndpointMetadata { Source = MetadataSource.Caller }, - new Attribute1(), - new Attribute2() + new CustomEndpointMetadata { Source = MetadataSource.Caller } } }; @@ -4649,21 +4697,8 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen var result = RequestDelegateFactory.Create(@delegate, options); // Assert - Assert.Collection(result.EndpointMetadata, - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m), - m => - { - Assert.IsAssignableFrom(m); - Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source); - }, - m => - { - Assert.IsAssignableFrom(m); - Assert.Equal(MetadataSource.Caller, ((CustomEndpointMetadata)m).Source); - }, - m => Assert.IsAssignableFrom(m), - m => Assert.IsAssignableFrom(m)); + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); + Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" }); } [Fact] @@ -4671,16 +4706,58 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplemen { // Arrange var @delegate = (AddsCustomParameterMetadata param1) => "Hello"; - var options = new RequestDelegateFactoryOptions { AdditionalEndpointMetadata = new List { new Attribute1() } }; + var options = new RequestDelegateFactoryOptions + { + InitialEndpointMetadata = new List + { + new CustomEndpointMetadata { Source = MetadataSource.Caller } + } + }; // Act var result = RequestDelegateFactory.Create(@delegate, options); // Assert - Assert.Contains(result.EndpointMetadata, m => m is Attribute1); + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter }); } + [Fact] + public void Create_CombinesAllMetadata_InCorrectOrder() + { + // Arrange + var @delegate = [Attribute1, Attribute2] (AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataResult(); + var options = new RequestDelegateFactoryOptions + { + InitialEndpointMetadata = new List + { + new CustomEndpointMetadata { Source = MetadataSource.Caller } + } + }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Collection(result.EndpointMetadata, + // MethodInfo + m => Assert.IsAssignableFrom(m), + // Initial metadata from RequestDelegateFactoryOptions.InitialEndpointMetadata + 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)), + // 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: 5 }), + // Handler delegate attributes + m => Assert.IsAssignableFrom(m), // NullableContextAttribute + m => Assert.IsType(m), + m => Assert.IsType(m)); + } + [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider() { @@ -4834,8 +4911,6 @@ public static void PopulateMetadata(EndpointMetadataContext context) private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => default; - public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext) { parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); @@ -4847,18 +4922,18 @@ public static void PopulateMetadata(EndpointMetadataContext context) } } - private class AddsNoCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider + private class AddsCustomParameterMetadataBindable : 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) { - + parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name }); } public static void PopulateMetadata(EndpointMetadataContext context) { - + context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter }); } } diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index cf2922db42cd..8ae59a2b4c0e 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -460,7 +460,7 @@ private static RouteHandlerBuilder Map( RoutePattern pattern, Delegate handler, bool disableInferBodyFromParameters, - IEnumerable? additionalEndpointMetadata = null) + IEnumerable? initialEndpointMetadata = null) { if (endpoints is null) { @@ -479,8 +479,6 @@ private static RouteHandlerBuilder Map( const int defaultOrder = 0; - var additionalMetadata = new List(); - var routeParams = new List(pattern.Parameters.Count); foreach (var part in pattern.Parameters) { @@ -496,25 +494,16 @@ private static RouteHandlerBuilder Map( DisplayName = pattern.RawText ?? pattern.DebuggerToString(), }; - // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are - // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? - - // Defer adding handler attributes as metadata to the call to RDF. RDF should add this metadata - // after any inferred metadata so that the additional metadata has a higher specificity. - var attributes = handler.Method.GetCustomAttributes(); - - // This can be null if the delegate is a dynamic method or compiled from an expression tree - if (attributes is not null) - { - additionalMetadata.AddRange(attributes); - } + //var attributes = handler.Method.GetCustomAttributes(); - // Add additional metadata provided by caller, deferred to the call to RDF. RDF should add this metadata - // after any inferred metadata so that the additional metadata has a higher specificity. - if (additionalEndpointMetadata is not null) - { - additionalMetadata.AddRange(additionalEndpointMetadata); - } + //// This can be null if the delegate is a dynamic method or compiled from an expression tree + //if (attributes is not null) + //{ + // foreach (var attr in attributes) + // { + // builder.Metadata.Add(attr); + // } + //} // Methods defined in a top-level program are generated as statics so the delegate // target will be null. Inline lambdas are compiler generated method so they can @@ -543,7 +532,7 @@ private static RouteHandlerBuilder Map( ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, DisableInferBodyFromParameters = disableInferBodyFromParameters, RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories, - AdditionalEndpointMetadata = additionalMetadata + InitialEndpointMetadata = initialEndpointMetadata }; var filteredRequestDelegateResult = RequestDelegateFactory.Create(handler, options); diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index b9d4e586076f..90aaf401d54d 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -81,8 +81,8 @@ void TestAction() static string GetMethod(IHttpMethodMetadata metadata) => Assert.Single(metadata.HttpMethods); Assert.Equal(3, metadataArray.Length); - Assert.Equal("ATTRIBUTE", GetMethod(metadataArray[0])); - Assert.Equal("METHOD", GetMethod(metadataArray[1])); + Assert.Equal("METHOD", GetMethod(metadataArray[0])); + Assert.Equal("ATTRIBUTE", GetMethod(metadataArray[1])); Assert.Equal("BUILDER", GetMethod(metadataArray[2])); Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata()!.HttpMethods.Single()); From 0ffe8bd9f1f8b4a96f6d50d3827c63ce44ede623 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 11:08:23 -0700 Subject: [PATCH 17/20] PR feedback --- src/Http/Http.Extensions/src/EndpointMetadataContext.cs | 4 ++-- .../src/EndpointParameterMetadataContext.cs | 4 ++-- src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs | 6 ++++++ .../src/IEndpointParameterMetadataProvider.cs | 8 +++++++- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 4 ++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs index c7ac2e457f09..a09da3be1044 100644 --- a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs @@ -21,7 +21,7 @@ public sealed class EndpointMetadataContext public IServiceProvider? Services { get; init; } /// - /// Gets the objects that will be added to the metadata of the endpoint. + /// Gets the list of objects that will be added to the metadata of the endpoint. /// - public IList? EndpointMetadata { get; init; } + public IList EndpointMetadata { get; init; } = new List(); } diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index 861964e5a9a5..220489d83f4b 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -21,7 +21,7 @@ public class EndpointParameterMetadataContext public IServiceProvider? Services { get; init; } /// - /// Gets the objects that will be added to the metadata of the endpoint. + /// Gets the list of objects that will be added to the metadata of the endpoint. /// - public IList? EndpointMetadata { get; init; } + public IList EndpointMetadata { get; init; } = new List(); } diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs index 7ffdcc4c49d9..b7bda01e4716 100644 --- a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs +++ b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs @@ -12,6 +12,12 @@ 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 index 0c7731154271..45c29dbbf347 100644 --- a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs +++ b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Http.Metadata; /// -/// Indicates that a type provides a static method that returns metadata when declared as the +/// 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 @@ -12,6 +12,12 @@ 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/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 481648080c35..201c9b171b9a 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList? +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext() -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo? @@ -8,7 +8,7 @@ Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo? From 4c93fe71b4e21191fb301ab05faf1b57452512a2 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 11:10:10 -0700 Subject: [PATCH 18/20] Remove commented out code --- .../src/Builder/EndpointRouteBuilderExtensions.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 8ae59a2b4c0e..ed1a94a4ac12 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -494,17 +494,6 @@ private static RouteHandlerBuilder Map( DisplayName = pattern.RawText ?? pattern.DebuggerToString(), }; - //var attributes = handler.Method.GetCustomAttributes(); - - //// This can be null if the delegate is a dynamic method or compiled from an expression tree - //if (attributes is not null) - //{ - // foreach (var attr in attributes) - // { - // builder.Metadata.Add(attr); - // } - //} - // Methods defined in a top-level program are generated as statics so the delegate // target will be null. Inline lambdas are compiler generated method so they can // be filtered that way. From de7d92605c2994b34f450072bc795ce150f6c7e2 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 12:14:25 -0700 Subject: [PATCH 19/20] Seal EndpointParameterMetadataContext --- .../Http.Extensions/src/EndpointParameterMetadataContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index 220489d83f4b..509251048ae3 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Http.Metadata; /// /// Represents the information accessible during endpoint creation by types that implement . /// -public class EndpointParameterMetadataContext +public sealed class EndpointParameterMetadataContext { /// /// Gets the parameter of the route handler delegate of the endpoint being created. From 7e8273a38824c89f94d10895d8908ccaef47a9f1 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 14:09:26 -0700 Subject: [PATCH 20/20] PR feedback --- src/Http/Http.Extensions/src/EndpointMetadataContext.cs | 4 ++-- .../Http.Extensions/src/EndpointParameterMetadataContext.cs | 4 ++-- src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs index a09da3be1044..5c6d4bb4c60a 100644 --- a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs @@ -13,7 +13,7 @@ public sealed class EndpointMetadataContext /// /// Gets the associated with the current route handler. /// - public MethodInfo? Method { get; init; } + public MethodInfo Method { get; init; } = null!; // Is initialized when created by RequestDelegateFactory /// /// Gets the instance used to access application services. @@ -23,5 +23,5 @@ public sealed class EndpointMetadataContext /// /// Gets the list of objects that will be added to the metadata of the endpoint. /// - public IList EndpointMetadata { get; init; } = new List(); + public IList EndpointMetadata { get; init; } = null!; // Is initialized when created by RequestDelegateFactory } diff --git a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs index 509251048ae3..1f7d5445ed1a 100644 --- a/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs +++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs @@ -13,7 +13,7 @@ public sealed class EndpointParameterMetadataContext /// /// Gets the parameter of the route handler delegate of the endpoint being created. /// - public ParameterInfo? Parameter { get; init; } + public ParameterInfo Parameter { get; init; } = null!; // Is initialized when created by RequestDelegateFactory /// /// Gets the associated with the current route handler. @@ -23,5 +23,5 @@ public sealed class EndpointParameterMetadataContext /// /// Gets the list of objects that will be added to the metadata of the endpoint. /// - public IList EndpointMetadata { get; init; } = new List(); + public IList EndpointMetadata { get; init; } = null!; // Is initialized when created by RequestDelegateFactory } diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 201c9b171b9a..b6e83b95e2cb 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext() -> void -Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo? +Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo! Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.init -> void @@ -11,7 +11,7 @@ Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void -Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo? +Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo! Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.init -> void Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.init -> void