Skip to content

Commit e0549fc

Browse files
Implement IEndpointMetadataProvider & IEndpointParameterMetadataProvider (#40926)
Fixes #40646
1 parent 1bc4ba6 commit e0549fc

11 files changed

+709
-28
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
6+
namespace Microsoft.AspNetCore.Http.Metadata;
7+
8+
/// <summary>
9+
/// Represents the information accessible during endpoint creation by types that implement <see cref="IEndpointMetadataProvider"/>.
10+
/// </summary>
11+
public sealed class EndpointMetadataContext
12+
{
13+
/// <summary>
14+
/// Gets the <see cref="MethodInfo"/> associated with the current route handler.
15+
/// </summary>
16+
public MethodInfo Method { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
17+
18+
/// <summary>
19+
/// Gets the <see cref="IServiceProvider"/> instance used to access application services.
20+
/// </summary>
21+
public IServiceProvider? Services { get; init; }
22+
23+
/// <summary>
24+
/// Gets the list of objects that will be added to the metadata of the endpoint.
25+
/// </summary>
26+
public IList<object> EndpointMetadata { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
6+
namespace Microsoft.AspNetCore.Http.Metadata;
7+
8+
/// <summary>
9+
/// Represents the information accessible during endpoint creation by types that implement <see cref="IEndpointParameterMetadataProvider"/>.
10+
/// </summary>
11+
public sealed class EndpointParameterMetadataContext
12+
{
13+
/// <summary>
14+
/// Gets the parameter of the route handler delegate of the endpoint being created.
15+
/// </summary>
16+
public ParameterInfo Parameter { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
17+
18+
/// <summary>
19+
/// Gets the <see cref="MethodInfo"/> associated with the current route handler.
20+
/// </summary>
21+
public IServiceProvider? Services { get; init; }
22+
23+
/// <summary>
24+
/// Gets the list of objects that will be added to the metadata of the endpoint.
25+
/// </summary>
26+
public IList<object> EndpointMetadata { get; init; } = null!; // Is initialized when created by RequestDelegateFactory
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Indicates that a type provides a static method that provides <see cref="Endpoint"/> metadata when declared as a parameter type or the
8+
/// returned type of an <see cref="Endpoint"/> route handler delegate.
9+
/// </summary>
10+
public interface IEndpointMetadataProvider
11+
{
12+
/// <summary>
13+
/// Populates metadata for the related <see cref="Endpoint"/>.
14+
/// </summary>
15+
/// <remarks>
16+
/// This method is called by <see cref="RequestDelegateFactory"/> when creating a <see cref="RequestDelegate"/>.
17+
/// The <see cref="EndpointMetadataContext.EndpointMetadata"/> property of <paramref name="context"/> will contain
18+
/// the initial metadata for the endpoint.<br />
19+
/// Add or remove objects on <see cref="EndpointMetadataContext.EndpointMetadata"/> to affect the metadata of the endpoint.
20+
/// </remarks>
21+
/// <param name="context">The <see cref="EndpointMetadataContext"/>.</param>
22+
static abstract void PopulateMetadata(EndpointMetadataContext context);
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Indicates that a type provides a static method that provides <see cref="Endpoint"/> metadata when declared as the
8+
/// parameter type of an <see cref="Endpoint"/> route handler delegate.
9+
/// </summary>
10+
public interface IEndpointParameterMetadataProvider
11+
{
12+
/// <summary>
13+
/// Populates metadata for the related <see cref="Endpoint"/>.
14+
/// </summary>
15+
/// <remarks>
16+
/// This method is called by <see cref="RequestDelegateFactory"/> when creating a <see cref="RequestDelegate"/>.
17+
/// The <see cref="EndpointParameterMetadataContext.EndpointMetadata"/> property of <paramref name="parameterContext"/> will contain
18+
/// the initial metadata for the endpoint.<br />
19+
/// Add or remove objects on <see cref="EndpointParameterMetadataContext.EndpointMetadata"/> to affect the metadata of the endpoint.
20+
/// </remarks>
21+
/// <param name="parameterContext">The <see cref="EndpointParameterMetadataContext"/>.</param>
22+
static abstract void PopulateMetadata(EndpointParameterMetadataContext parameterContext);
23+
}

src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext
3+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList<object!>!
4+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.init -> void
5+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadataContext() -> void
6+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.get -> System.Reflection.MethodInfo!
7+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Method.init -> void
8+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.get -> System.IServiceProvider?
9+
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.Services.init -> void
10+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext
11+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList<object!>!
12+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointMetadata.init -> void
13+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.EndpointParameterMetadataContext() -> void
14+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.get -> System.Reflection.ParameterInfo!
15+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Parameter.init -> void
16+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.get -> System.IServiceProvider?
17+
Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext.Services.init -> void
18+
Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider
19+
Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext! context) -> void
20+
Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider
21+
Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider.PopulateMetadata(Microsoft.AspNetCore.Http.Metadata.EndpointParameterMetadataContext! parameterContext) -> void
22+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata.get -> System.Collections.Generic.IEnumerable<object!>?
23+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.InitialEndpointMetadata.init -> void
224
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList<System.Func<Microsoft.AspNetCore.Http.RouteHandlerContext!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!>!>?
325
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void
426
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public static partial class RequestDelegateFactory
4747
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
4848
private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!;
4949
private static readonly MethodInfo WrapObjectAsValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WrapObjectAsValueTask), BindingFlags.NonPublic | BindingFlags.Static)!;
50+
private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!;
51+
private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!;
5052

5153
// Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type.
5254
// https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
@@ -165,16 +167,26 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func<HttpConte
165167
return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata);
166168
}
167169

168-
private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) =>
169-
new()
170+
private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options)
171+
{
172+
var context = new FactoryContext
170173
{
174+
ServiceProvider = options?.ServiceProvider,
171175
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>(),
172176
RouteParameters = options?.RouteParameterNames?.ToList(),
173177
ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false,
174178
DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false,
175179
Filters = options?.RouteHandlerFilterFactories?.ToList()
176180
};
177181

182+
if (options?.InitialEndpointMetadata is not null)
183+
{
184+
context.Metadata.AddRange(options.InitialEndpointMetadata);
185+
}
186+
187+
return context;
188+
}
189+
178190
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext)
179191
{
180192
// Non void return type
@@ -193,10 +205,20 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
193205
// return default;
194206
// }
195207

208+
// Add MethodInfo as first metadata item
209+
factoryContext.Metadata.Insert(0, methodInfo);
210+
211+
// CreateArguments will add metadata inferred from parameter details
196212
var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext);
197213
var returnType = methodInfo.ReturnType;
198214
factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments);
199215

216+
// Add metadata provided by the delegate return type and parameter types next, this will be more specific than inferred metadata from above
217+
AddTypeProvidedMetadata(methodInfo, factoryContext.Metadata, factoryContext.ServiceProvider);
218+
219+
// Add method attributes as metadata *after* any inferred metadata so that the attributes hava a higher specificity
220+
AddMethodAttributesAsMetadata(methodInfo, factoryContext.Metadata);
221+
200222
// If there are filters registered on the route handler, then we update the method call and
201223
// return type associated with the request to allow for the filter invocation pipeline.
202224
if (factoryContext.Filters is { Count: > 0 })
@@ -261,6 +283,82 @@ target is null
261283
return filteredInvocation;
262284
}
263285

286+
private static void AddTypeProvidedMetadata(MethodInfo methodInfo, List<object> metadata, IServiceProvider? services)
287+
{
288+
object?[]? invokeArgs = null;
289+
290+
// Get metadata from parameter types
291+
var parameters = methodInfo.GetParameters();
292+
foreach (var parameter in parameters)
293+
{
294+
if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType))
295+
{
296+
// Parameter type implements IEndpointParameterMetadataProvider
297+
var parameterContext = new EndpointParameterMetadataContext
298+
{
299+
Parameter = parameter,
300+
EndpointMetadata = metadata,
301+
Services = services
302+
};
303+
invokeArgs ??= new object[1];
304+
invokeArgs[0] = parameterContext;
305+
PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs);
306+
}
307+
308+
if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType))
309+
{
310+
// Parameter type implements IEndpointMetadataProvider
311+
var context = new EndpointMetadataContext
312+
{
313+
Method = methodInfo,
314+
EndpointMetadata = metadata,
315+
Services = services
316+
};
317+
invokeArgs ??= new object[1];
318+
invokeArgs[0] = context;
319+
PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs);
320+
}
321+
}
322+
323+
// Get metadata from return type
324+
if (methodInfo.ReturnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(methodInfo.ReturnType))
325+
{
326+
// Return type implements IEndpointMetadataProvider
327+
var context = new EndpointMetadataContext
328+
{
329+
Method = methodInfo,
330+
EndpointMetadata = metadata,
331+
Services = services
332+
};
333+
invokeArgs ??= new object[1];
334+
invokeArgs[0] = context;
335+
PopulateMetadataForEndpointMethod.MakeGenericMethod(methodInfo.ReturnType).Invoke(null, invokeArgs);
336+
}
337+
}
338+
339+
private static void PopulateMetadataForParameter<T>(EndpointParameterMetadataContext parameterContext)
340+
where T : IEndpointParameterMetadataProvider
341+
{
342+
T.PopulateMetadata(parameterContext);
343+
}
344+
345+
private static void PopulateMetadataForEndpoint<T>(EndpointMetadataContext context)
346+
where T : IEndpointMetadataProvider
347+
{
348+
T.PopulateMetadata(context);
349+
}
350+
351+
private static void AddMethodAttributesAsMetadata(MethodInfo methodInfo, List<object> metadata)
352+
{
353+
var attributes = methodInfo.GetCustomAttributes();
354+
355+
// This can be null if the delegate is a dynamic method or compiled from an expression tree
356+
if (attributes is not null)
357+
{
358+
metadata.AddRange(attributes);
359+
}
360+
}
361+
264362
private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext)
265363
{
266364
if (parameters is null || parameters.Length == 0)
@@ -1679,6 +1777,7 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex
16791777
private class FactoryContext
16801778
{
16811779
// Options
1780+
public IServiceProvider? ServiceProvider { get; init; }
16821781
public IServiceProviderIsService? ServiceProviderIsService { get; init; }
16831782
public List<string>? RouteParameters { get; init; }
16841783
public bool ThrowOnBadRequest { get; init; }
@@ -1697,7 +1796,7 @@ private class FactoryContext
16971796
public bool HasMultipleBodyParameters { get; set; }
16981797
public bool HasInferredBody { get; set; }
16991798

1700-
public List<object> Metadata { get; } = new();
1799+
public List<object> Metadata { get; internal set; } = new();
17011800

17021801
public NullabilityInfoContext NullabilityContext { get; } = new();
17031802

src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http;
1212
public sealed class RequestDelegateFactoryOptions
1313
{
1414
/// <summary>
15-
/// The <see cref="IServiceProvider"/> instance used to detect if handler parameters are services.
15+
/// The <see cref="IServiceProvider"/> instance used to access application services.
1616
/// </summary>
1717
public IServiceProvider? ServiceProvider { get; init; }
1818

@@ -36,4 +36,15 @@ public sealed class RequestDelegateFactoryOptions
3636
/// The list of filters that must run in the pipeline for a given route handler.
3737
/// </summary>
3838
public IReadOnlyList<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? RouteHandlerFilterFactories { get; init; }
39+
40+
/// <summary>
41+
/// The initial endpoint metadata to add as part of the creation of the <see cref="RequestDelegateResult.RequestDelegate"/>.
42+
/// </summary>
43+
/// <remarks>
44+
/// This metadata will be included in <see cref="RequestDelegateResult.EndpointMetadata" /> <b>before</b> any metadata inferred during creation of the
45+
/// <see cref="RequestDelegateResult.RequestDelegate"/> and <b>before</b> any metadata provided by types in the delegate signature that implement
46+
/// <see cref="IEndpointMetadataProvider" /> or <see cref="IEndpointParameterMetadataProvider" />, i.e. this metadata will be less specific than any
47+
/// inferred by the call to <see cref="RequestDelegateFactory.Create(Delegate, RequestDelegateFactoryOptions?)"/>.
48+
/// </remarks>
49+
public IEnumerable<object>? InitialEndpointMetadata { get; init; }
3950
}

0 commit comments

Comments
 (0)