Skip to content

Improve Minimal APIs support for request media types #35082 #35230

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 34 commits into from
Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f855193
add support for request media types
rafikiassumani-msft Aug 10, 2021
8133bff
change namespace for acceptsmatcher policy
rafikiassumani-msft Aug 11, 2021
17853dd
additional changes
rafikiassumani-msft Aug 11, 2021
a24a181
enable 415 when unsupported content type is provide
rafikiassumani-msft Aug 11, 2021
2116e6d
add accepts extension method on minimalActions endpoint
rafikiassumani-msft Aug 11, 2021
6700e93
add IAcceptsMetadata to API description
rafikiassumani-msft Aug 12, 2021
094ed22
add empty content type test
rafikiassumani-msft Aug 12, 2021
ab29717
feat: add types for iacceptmetadata
rafikiassumani-msft Aug 13, 2021
94fa1a7
change requestdelegate factory to return metatdata
rafikiassumani-msft Aug 13, 2021
8096fd3
clean RequestDelegateFactoryOptions.cs
rafikiassumani-msft Aug 13, 2021
ce440cb
change request delegate to return requestdelegateresult type
rafikiassumani-msft Aug 13, 2021
74b234b
make apis property init only
rafikiassumani-msft Aug 13, 2021
990be74
adding constructor to requestdelegatefactoryResult
rafikiassumani-msft Aug 13, 2021
0097323
Fixups
pranavkm Aug 14, 2021
6b108eb
resolve conflicts
rafikiassumani-msft Aug 16, 2021
d4e85d5
fix merge errors
rafikiassumani-msft Aug 16, 2021
3de78c6
address pr comment
rafikiassumani-msft Aug 16, 2021
c3d2cd3
fix test error
rafikiassumani-msft Aug 16, 2021
9ec2bcd
remove options from params
rafikiassumani-msft Aug 16, 2021
9d440ab
implements iacceptsMetadata
rafikiassumani-msft Aug 18, 2021
78667ac
merge conflicts
rafikiassumani-msft Aug 18, 2021
be64e8e
fix test failures
rafikiassumani-msft Aug 19, 2021
85b38ca
fix test failures
rafikiassumani-msft Aug 19, 2021
f6762c9
move iacceptmetadata to shared source
rafikiassumani-msft Aug 19, 2021
d4daaa7
add acceptsmetadata shared code to mvc
rafikiassumani-msft Aug 19, 2021
ad8ed0c
resolve conflicts
rafikiassumani-msft Aug 19, 2021
f43d3c0
fix tests
rafikiassumani-msft Aug 19, 2021
7b664f8
fix merge conflicts
rafikiassumani-msft Aug 20, 2021
2b638db
address pr comments
rafikiassumani-msft Aug 20, 2021
14a7ff9
address another comment
rafikiassumani-msft Aug 20, 2021
38790ae
nit
rafikiassumani-msft Aug 20, 2021
8a8348e
fix duplicate media types
rafikiassumani-msft Aug 20, 2021
88f6c6c
resolve merge conflicts
rafikiassumani-msft Aug 20, 2021
b29ea2a
fix test failures
rafikiassumani-msft Aug 20, 2021
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