diff --git a/src/Http/Http.Extensions/src/EndpointMetadataContext.cs b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs
new file mode 100644
index 000000000000..5c6d4bb4c60a
--- /dev/null
+++ b/src/Http/Http.Extensions/src/EndpointMetadataContext.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+
+namespace Microsoft.AspNetCore.Http.Metadata;
+
+///
+/// Represents the information accessible during endpoint creation by types that implement .
+///
+public sealed class EndpointMetadataContext
+{
+ ///
+ /// Gets the associated with the current route handler.
+ ///
+ public MethodInfo Method { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
+
+ ///
+ /// Gets the instance used to access application services.
+ ///
+ public IServiceProvider? Services { get; init; }
+
+ ///
+ /// Gets the list of objects that will be added to the metadata of the endpoint.
+ ///
+ 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
new file mode 100644
index 000000000000..1f7d5445ed1a
--- /dev/null
+++ b/src/Http/Http.Extensions/src/EndpointParameterMetadataContext.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+
+namespace Microsoft.AspNetCore.Http.Metadata;
+
+///
+/// Represents the information accessible during endpoint creation by types that implement .
+///
+public sealed class EndpointParameterMetadataContext
+{
+ ///
+ /// Gets the parameter of the route handler delegate of the endpoint being created.
+ ///
+ public ParameterInfo Parameter { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
+
+ ///
+ /// Gets the associated with the current route handler.
+ ///
+ public IServiceProvider? Services { get; init; }
+
+ ///
+ /// Gets the list of objects that will be added to the metadata of the endpoint.
+ ///
+ public IList EndpointMetadata { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
+}
diff --git a/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs
new file mode 100644
index 000000000000..b7bda01e4716
--- /dev/null
+++ b/src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http.Metadata;
+
+///
+/// Indicates that a type provides a static method that provides metadata when declared as a parameter type or the
+/// returned type of an route handler delegate.
+///
+public interface IEndpointMetadataProvider
+{
+ ///
+ /// Populates metadata for the related .
+ ///
+ ///
+ /// This method is called by when creating a .
+ /// The property of will contain
+ /// the initial metadata for the endpoint.
+ /// Add or remove objects on to affect the metadata of the endpoint.
+ ///
+ /// The .
+ static abstract void PopulateMetadata(EndpointMetadataContext context);
+}
diff --git a/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs
new file mode 100644
index 000000000000..45c29dbbf347
--- /dev/null
+++ b/src/Http/Http.Extensions/src/IEndpointParameterMetadataProvider.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http.Metadata;
+
+///
+/// Indicates that a type provides a static method that provides metadata when declared as the
+/// parameter type of an route handler delegate.
+///
+public interface IEndpointParameterMetadataProvider
+{
+ ///
+ /// Populates metadata for the related .
+ ///
+ ///
+ /// This method is called by when creating a .
+ /// The property of will contain
+ /// the initial metadata for the endpoint.
+ /// Add or remove objects on to affect the metadata of the endpoint.
+ ///
+ /// The .
+ static abstract void PopulateMetadata(EndpointParameterMetadataContext parameterContext);
+}
diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
index f5825c4e8476..b6e83b95e2cb 100644
--- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
@@ -1,4 +1,26 @@
#nullable enable
+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.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.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 c32a6a790147..b7a9b7fd111b 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
@@ -159,9 +161,11 @@ 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,
@@ -169,6 +173,14 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
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
@@ -187,10 +199,20 @@ 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 delegate return type and parameter types next, this will be more specific than inferred metadata from above
+ AddTypeProvidedMetadata(methodInfo, factoryContext.Metadata, factoryContext.ServiceProvider);
+
+ // 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.
if (factoryContext.Filters is { Count: > 0 })
@@ -255,6 +277,82 @@ target is null
return filteredInvocation;
}
+ private static void AddTypeProvidedMetadata(MethodInfo methodInfo, List metadata, IServiceProvider? services)
+ {
+ 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
+ var parameterContext = new EndpointParameterMetadataContext
+ {
+ Parameter = parameter,
+ EndpointMetadata = metadata,
+ Services = services
+ };
+ invokeArgs ??= new object[1];
+ invokeArgs[0] = parameterContext;
+ PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs);
+ }
+
+ if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType))
+ {
+ // Parameter type implements IEndpointMetadataProvider
+ var context = new EndpointMetadataContext
+ {
+ Method = methodInfo,
+ EndpointMetadata = metadata,
+ 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
+ var context = new EndpointMetadataContext
+ {
+ Method = methodInfo,
+ EndpointMetadata = metadata,
+ 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);
+ }
+
+ 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)
@@ -1669,6 +1767,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; }
@@ -1687,7 +1786,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 70207f9c63d8..9b367dfcfcf0 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,15 @@ 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 initial endpoint metadata to add as part of the creation of the .
+ ///
+ ///
+ /// 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? InitialEndpointMetadata { get; init; }
}
diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
index f597d15edc15..5eea1bcf753c 100644
--- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
+++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
@@ -4560,6 +4560,244 @@ 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 = (AddsCustomParameterMetadataBindable 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
+ {
+ InitialEndpointMetadata = new List
+ {
+ new CustomEndpointMetadata { Source = MetadataSource.Caller }
+ }
+ };
+
+ // Act
+ var result = RequestDelegateFactory.Create(@delegate, options);
+
+ // Assert
+ 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]
+ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider()
+ {
+ // Arrange
+ var @delegate = (AddsCustomParameterMetadata param1) => "Hello";
+ 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 CustomEndpointMetadata { Source: MetadataSource.Caller });
+ Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" });
+ }
+
+ [Fact]
+ public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointMetadataProvider()
+ {
+ // Arrange
+ var @delegate = (AddsCustomParameterMetadata param1) => "Hello";
+ 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 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()
+ {
+ // Arrange
+ var @delegate = (Todo todo) => new RemovesAcceptsMetadataResult();
+
+ // Act
+ var result = RequestDelegateFactory.Create(@delegate);
+
+ // Assert
+ Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata);
+ }
+
+ [Fact]
+ public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider()
+ {
+ // Arrange
+ var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello";
+
+ // Act
+ var result = RequestDelegateFactory.Create(@delegate);
+
+ // Assert
+ Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata);
+ }
+
+ [Fact]
+ public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointMetadataProvider()
+ {
+ // Arrange
+ var @delegate = (RemovesAcceptsParameterMetadata param1) => "Hello";
+ var options = new RequestDelegateFactoryOptions();
+
+ // Act
+ var result = RequestDelegateFactory.Create(@delegate, options);
+
+ // Assert
+ Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata);
+ }
+
private DefaultHttpContext CreateHttpContext()
{
var responseFeature = new TestHttpResponseFeature();
@@ -4576,6 +4814,153 @@ 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 ?? 0 });
+ }
+
+ public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException();
+ }
+
+ private class RemovesAcceptsParameterMetadata : IEndpointParameterMetadataProvider
+ {
+ public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext)
+ {
+ if (parameterContext.EndpointMetadata is not null)
+ {
+ for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--)
+ {
+ var metadata = parameterContext.EndpointMetadata[i];
+ if (metadata is IAcceptsMetadata)
+ {
+ parameterContext.EndpointMetadata.RemoveAt(i);
+ }
+ }
+ }
+ }
+ }
+
+ private class RemovesAcceptsMetadata : IEndpointMetadataProvider
+ {
+ public static void PopulateMetadata(EndpointMetadataContext parameterContext)
+ {
+ if (parameterContext.EndpointMetadata is not null)
+ {
+ for (int i = parameterContext.EndpointMetadata.Count - 1; i >= 0; i--)
+ {
+ var metadata = parameterContext.EndpointMetadata[i];
+ if (metadata is IAcceptsMetadata)
+ {
+ parameterContext.EndpointMetadata.RemoveAt(i);
+ }
+ }
+ }
+ }
+ }
+
+ private class RemovesAcceptsMetadataResult : IEndpointMetadataProvider, IResult
+ {
+ public static void PopulateMetadata(EndpointMetadataContext context)
+ {
+ if (context.EndpointMetadata is not null)
+ {
+ for (int i = context.EndpointMetadata.Count - 1; i >= 0; i--)
+ {
+ var metadata = context.EndpointMetadata[i];
+ if (metadata is IAcceptsMetadata)
+ {
+ context.EndpointMetadata.RemoveAt(i);
+ }
+ }
+ }
+ }
+
+ public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException();
+ }
+
+ private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider
+ {
+ public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext)
+ {
+ parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name });
+ }
+
+ public static void PopulateMetadata(EndpointMetadataContext context)
+ {
+ context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Parameter });
+ }
+ }
+
+ private class AddsCustomParameterMetadataBindable : 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 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
+ {
+ Caller,
+ 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 d8990104cb12..ed1a94a4ac12 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)
@@ -457,7 +459,8 @@ private static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate handler,
- bool disableInferBodyFromParameters)
+ bool disableInferBodyFromParameters,
+ IEnumerable? initialEndpointMetadata = null)
{
if (endpoints is null)
{
@@ -491,12 +494,6 @@ 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?
-
- // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint.
- builder.Metadata.Add(handler.Method);
-
// 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.
@@ -523,27 +520,17 @@ private static RouteHandlerBuilder Map(
RouteParameterNames = routeParams,
ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false,
DisableInferBodyFromParameters = disableInferBodyFromParameters,
- RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories
+ RouteHandlerFilterFactories = routeHandlerBuilder.RouteHandlerFilterFactories,
+ InitialEndpointMetadata = initialEndpointMetadata
};
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);
- }
- }
endpointBuilder.RequestDelegate = filteredRequestDelegateResult.RequestDelegate;
});
diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs
index df21fbb1d7f1..54f4b313ecc1 100644
--- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs
+++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs
@@ -3,9 +3,11 @@
using System.IO.Pipelines;
using System.Linq.Expressions;
+using System.Reflection;
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;
@@ -221,6 +223,38 @@ public void AddingMetadataAfterBuildingEndpointThrows(Func(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo")));
}
+ [Fact]
+ public void Map_AddsMetadata_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);
+ Assert.Equal(MetadataSource.Parameter, ((CustomEndpointMetadata)m).Source);
+ },
+ m =>
+ {
+ Assert.IsAssignableFrom(m);
+ Assert.Equal(MetadataSource.ReturnType, ((CustomEndpointMetadata)m).Source);
+ },
+ m => Assert.IsAssignableFrom(m),
+ m => Assert.IsAssignableFrom(m));
+ }
+
[Attribute1]
[Attribute2]
private static Task Handle(HttpContext context) => Task.CompletedTask;
@@ -247,4 +281,47 @@ 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 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 ParameterNameMetadata
+ {
+ public string Name { get; init; }
+ }
+
+ private class CustomEndpointMetadata
+ {
+ public string Data { get; init; }
+
+ public MetadataSource Source { get; init; }
+ }
+
+ private enum MetadataSource
+ {
+ Parameter,
+ ReturnType
+ }
}
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());