Skip to content

Improve Minimal APIs support for request media types #35082 (#35230) #35579

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 1 commit into from
Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.


using System.Collections.Generic;

namespace Microsoft.AspNetCore.Http.Metadata
{
/// <summary>
/// Interface for accepting request media types.
/// </summary>
public interface IAcceptsMetadata
{
/// <summary>
/// Gets a list of the allowed request content types.
/// If the incoming request does not have a <c>Content-Type</c> with one of these values, the request will be rejected with a 415 response.
/// </summary>
IReadOnlyList<string> ContentTypes { get; }

/// <summary>
/// Gets the type being read from the request.
/// </summary>
Type? RequestType { get; }
}
}
7 changes: 7 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string!
Microsoft.AspNetCore.Http.IResult
Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type?
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool
Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata
Expand All @@ -18,6 +21,10 @@ Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string?
Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata
Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void
Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate?
Microsoft.AspNetCore.Http.RequestDelegateResult
Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList<object!>!
Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate!
Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, System.Collections.Generic.IReadOnlyList<object!>! metadata) -> void
Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool
static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string!
static Microsoft.AspNetCore.Http.HttpProtocol.IsHttp09(string! protocol) -> bool
Expand Down
33 changes: 33 additions & 0 deletions src/Http/Http.Abstractions/src/RequestDelegateResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// The result of creating a <see cref="RequestDelegate" /> from a <see cref="Delegate" />
/// </summary>
public sealed class RequestDelegateResult
{
/// <summary>
/// Creates a new instance of <see cref="RequestDelegateResult"/>.
/// </summary>
public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList<object> metadata)
{
RequestDelegate = requestDelegate;
EndpointMetadata = metadata;
}

/// <summary>
/// Gets the <see cref="RequestDelegate" />
/// </summary>
public RequestDelegate RequestDelegate { get;}

/// <summary>
/// Gets endpoint metadata inferred from creating the <see cref="RequestDelegate" />
/// </summary>
public IReadOnlyList<object> EndpointMetadata { get;}
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state.</Description>
Expand All @@ -16,6 +16,7 @@
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ProblemDetailsJsonConverter.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)HttpValidationProblemDetailsJsonConverter.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList<T>(th
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders!
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders!
static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string?
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate!
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func<Microsoft.AspNetCore.Http.HttpContext!, object!>? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate!
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult!
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func<Microsoft.AspNetCore.Http.HttpContext!, object!>? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult!
static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void
static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void
static Microsoft.AspNetCore.Http.SendFileResponseExtensions.SendFileAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, Microsoft.Extensions.FileProviders.IFileInfo! file, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Expand Down
49 changes: 26 additions & 23 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Reflection;
using System.Security.Claims;
using System.Text;
Expand Down Expand Up @@ -63,14 +64,16 @@ public static partial class RequestDelegateFactory
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));

private static readonly AcceptsMetadata DefaultAcceptsMetadata = new(new[] { "application/json" });

/// <summary>
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
/// </summary>
/// <param name="action">A request handler with any number of custom parameters that often produces a response with its return value.</param>
/// <param name="options">The <see cref="RequestDelegateFactoryOptions"/> used to configure the behavior of the handler.</param>
/// <returns>The <see cref="RequestDelegate"/>.</returns>
/// <returns>The <see cref="RequestDelegateResult"/>.</returns>
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOptions? options = null)
public static RequestDelegateResult Create(Delegate action, RequestDelegateFactoryOptions? options = null)
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
{
if (action is null)
Expand All @@ -84,12 +87,15 @@ public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOpti
null => null,
};

var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, targetExpression);

return httpContext =>
var factoryContext = new FactoryContext
{
return targetableRequestDelegate(action.Target, httpContext);
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
};

var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, factoryContext, targetExpression);

return new RequestDelegateResult(httpContext => targetableRequestDelegate(action.Target, httpContext), factoryContext.Metadata);

}

/// <summary>
Expand All @@ -100,7 +106,7 @@ public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOpti
/// <param name="options">The <see cref="RequestDelegateFactoryOptions"/> used to configure the behavior of the handler.</param>
/// <returns>The <see cref="RequestDelegate"/>.</returns>
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, object>? targetFactory = null, RequestDelegateFactoryOptions? options = null)
public static RequestDelegateResult Create(MethodInfo methodInfo, Func<HttpContext, object>? targetFactory = null, RequestDelegateFactoryOptions? options = null)
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
{
if (methodInfo is null)
Expand All @@ -113,31 +119,30 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, ob
throw new ArgumentException($"{nameof(methodInfo)} does not have a declaring type.");
}

var factoryContext = new FactoryContext
{
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
};

if (targetFactory is null)
{
if (methodInfo.IsStatic)
{
var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression: null);
var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression: null);

return httpContext =>
{
return untargetableRequestDelegate(null, httpContext);
};
return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata);
}

targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!;
}

var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType);
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression);
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression);

return httpContext =>
{
return targetableRequestDelegate(targetFactory(httpContext), httpContext);
};
return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata);
}

private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, Expression? targetExpression)
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression)
{
// Non void return type

Expand All @@ -155,11 +160,6 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, ob
// return default;
// }

var factoryContext = new FactoryContext()
{
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
};

if (options?.RouteParameterNames is { } routeParameterNames)
{
factoryContext.RouteParameters = new(routeParameterNames);
Expand Down Expand Up @@ -861,6 +861,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
}
}

factoryContext.Metadata.Add(DefaultAcceptsMetadata);
var isOptional = IsOptionalParameter(parameter);

factoryContext.JsonRequestBodyType = parameter.ParameterType;
Expand Down Expand Up @@ -1111,6 +1112,8 @@ private class FactoryContext

public Dictionary<string, string> TrackedParameters { get; } = new();
public bool HasMultipleBodyParameters { get; set; }

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

private static class RequestDelegateFactoryConstants
Expand Down
Loading