Skip to content

Implement IProvideEndpointMetadata & IProvideEndpointParameterMetadata #40926

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Http/Http.Extensions/src/EndpointMetadataContext.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents the information accessible during endpoint creation by types that implement <see cref="IEndpointMetadataProvider"/>.
/// </summary>
public sealed class EndpointMetadataContext
{
/// <summary>
/// Gets the <see cref="MethodInfo"/> associated with the current route handler.
/// </summary>
public MethodInfo? Method { get; init; }

/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance used to access application services.
/// </summary>
public IServiceProvider? Services { get; init; }

/// <summary>
/// Gets the objects that will be added to the metadata of the endpoint.
/// </summary>
public IList<object>? EndpointMetadata { get; init; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents the information accessible during endpoint creation by types that implement <see cref="IEndpointParameterMetadataProvider"/>.
/// </summary>
public class EndpointParameterMetadataContext
{
/// <summary>
/// Gets the parameter of the route handler delegate of the endpoint being created.
/// </summary>
public ParameterInfo? Parameter { get; set; } // set to allow re-use

/// <summary>
/// Gets the <see cref="MethodInfo"/> associated with the current route handler.
/// </summary>
public IServiceProvider? Services { get; init; }

/// <summary>
/// Gets the objects that will be added to the metadata of the endpoint.
/// </summary>
public IList<object>? EndpointMetadata { get; init; }
}
17 changes: 17 additions & 0 deletions src/Http/Http.Extensions/src/IEndpointMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -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.Metadata;

/// <summary>
/// Indicates that a type provides a static method that provides <see cref="Endpoint"/> metadata when declared as a parameter type or the
/// returned type of an <see cref="Endpoint"/> route handler delegate.
/// </summary>
public interface IEndpointMetadataProvider
{
/// <summary>
/// Populates metadata for the related <see cref="Endpoint"/>.
/// </summary>
/// <param name="context">The <see cref="EndpointMetadataContext"/>.</param>
static abstract void PopulateMetadata(EndpointMetadataContext context);
}
Original file line number Diff line number Diff line change
@@ -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.Metadata;

/// <summary>
/// Indicates that a type provides a static method that returns <see cref="Endpoint"/> metadata when declared as the
/// parameter type of an <see cref="Endpoint"/> route handler delegate.
/// </summary>
public interface IEndpointParameterMetadataProvider
{
/// <summary>
/// Populates metadata for the related <see cref="Endpoint"/>.
/// </summary>
/// <param name="parameterContext">The <see cref="EndpointParameterMetadataContext"/>.</param>
static abstract void PopulateMetadata(EndpointParameterMetadataContext parameterContext);
}
22 changes: 22 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
#nullable enable
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext
Microsoft.AspNetCore.Http.Metadata.EndpointMetadataContext.EndpointMetadata.get -> System.Collections.Generic.IList<object!>?
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<object!>?
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
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<object!>?
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.AdditionalEndpointMetadata.init -> void
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.get -> System.Collections.Generic.IReadOnlyList<System.Func<Microsoft.AspNetCore.Http.RouteHandlerContext!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate!>!>?
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteHandlerFilterFactories.init -> void
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
Expand Down
90 changes: 88 additions & 2 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object?>() 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
Expand Down Expand Up @@ -162,11 +164,13 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func<HttpConte
private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) =>
new()
{
ServiceProvider = options?.ServiceProvider,
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>(),
RouteParameters = options?.RouteParameterNames?.ToList(),
ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false,
DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false,
Filters = options?.RouteHandlerFilterFactories?.ToList()
Filters = options?.RouteHandlerFilterFactories?.ToList(),
AdditionalEndpointMetadata = options?.AdditionalEndpointMetadata
};

private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext)
Expand All @@ -187,10 +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 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 })
Expand Down Expand Up @@ -255,6 +272,73 @@ target is null
return filteredInvocation;
}

private static void AddTypeProvidedMetadata(MethodInfo methodInfo, IList<object> 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,
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
{
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
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<T>(EndpointParameterMetadataContext parameterContext)
where T : IEndpointParameterMetadataProvider
{
T.PopulateMetadata(parameterContext);
}

private static void PopulateMetadataForEndpoint<T>(EndpointMetadataContext context)
where T : IEndpointMetadataProvider
{
T.PopulateMetadata(context);
}

private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext)
{
if (parameters is null || parameters.Length == 0)
Expand Down Expand Up @@ -1669,10 +1753,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<string>? RouteParameters { get; init; }
public bool ThrowOnBadRequest { get; init; }
public bool DisableInferredFromBody { get; init; }
public IEnumerable<object>? AdditionalEndpointMetadata { get; init; }

// Temporary State
public ParameterInfo? JsonRequestBodyParameter { get; set; }
Expand All @@ -1687,7 +1773,7 @@ private class FactoryContext
public bool HasMultipleBodyParameters { get; set; }
public bool HasInferredBody { get; set; }

public List<object> Metadata { get; } = new();
public List<object> Metadata { get; internal set; } = new();

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

Expand Down
13 changes: 12 additions & 1 deletion src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http;
public sealed class RequestDelegateFactoryOptions
{
/// <summary>
/// The <see cref="IServiceProvider"/> instance used to detect if handler parameters are services.
/// The <see cref="IServiceProvider"/> instance used to access application services.
/// </summary>
public IServiceProvider? ServiceProvider { get; init; }

Expand All @@ -36,4 +36,15 @@ public sealed class RequestDelegateFactoryOptions
/// The list of filters that must run in the pipeline for a given route handler.
/// </summary>
public IReadOnlyList<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? RouteHandlerFilterFactories { get; init; }

/// <summary>
/// The additional endpoint metadata to add as part of the creation of the <see cref="RequestDelegateResult.RequestDelegate"/>.
/// </summary>
/// <remarks>
/// This metadata will be included in <see cref="RequestDelegateResult.EndpointMetadata" /> <b>after</b> any metadata inferred during creation of the
/// <see cref="RequestDelegateResult.RequestDelegate"/> and <b>after</b> any metadata provided by types in the delegate signature that implement
/// <see cref="IEndpointMetadataProvider" /> or <see cref="IEndpointParameterMetadataProvider" />, i.e. this metadata will be more specific than any
/// inferred by the call to <see cref="RequestDelegateFactory.Create(Delegate, RequestDelegateFactoryOptions?)"/>.
/// </remarks>
public IEnumerable<object>? AdditionalEndpointMetadata { get; init; }
}
Loading