Skip to content

Commit 4f5049a

Browse files
Improve Minimal APIs support for request media types #35082 (#35230)
* add support for request media types * change namespace for acceptsmatcher policy * additional changes * enable 415 when unsupported content type is provide * add accepts extension method on minimalActions endpoint * add IAcceptsMetadata to API description * add empty content type test * feat: add types for iacceptmetadata * change requestdelegate factory to return metatdata * clean RequestDelegateFactoryOptions.cs * change request delegate to return requestdelegateresult type * make apis property init only * adding constructor to requestdelegatefactoryResult * Fixups * fix merge errors * address pr comment * fix test error * remove options from params * implements iacceptsMetadata * fix test failures * fix test failures * move iacceptmetadata to shared source * add acceptsmetadata shared code to mvc * fix tests * address pr comments * address another comment * nit * fix duplicate media types * fix test failures Co-authored-by: Pranav K <[email protected]>
1 parent 9dff3d9 commit 4f5049a

35 files changed

+1839
-1012
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Http.Metadata
8+
{
9+
/// <summary>
10+
/// Interface for accepting request media types.
11+
/// </summary>
12+
public interface IAcceptsMetadata
13+
{
14+
/// <summary>
15+
/// Gets a list of the allowed request content types.
16+
/// 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.
17+
/// </summary>
18+
IReadOnlyList<string> ContentTypes { get; }
19+
20+
/// <summary>
21+
/// Gets the type being read from the request.
22+
/// </summary>
23+
Type? RequestType { get; }
24+
}
25+
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string!
88
Microsoft.AspNetCore.Http.IResult
99
Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
10+
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata
11+
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
12+
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type?
1013
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
1114
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool
1215
Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata
@@ -18,6 +21,10 @@ Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string?
1821
Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata
1922
Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void
2023
Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate?
24+
Microsoft.AspNetCore.Http.RequestDelegateResult
25+
Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList<object!>!
26+
Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate!
27+
Microsoft.AspNetCore.Http.RequestDelegateResult.RequestDelegateResult(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, System.Collections.Generic.IReadOnlyList<object!>! metadata) -> void
2128
Microsoft.AspNetCore.Routing.RouteValueDictionary.TryAdd(string! key, object? value) -> bool
2229
static readonly Microsoft.AspNetCore.Http.HttpProtocol.Http09 -> string!
2330
static Microsoft.AspNetCore.Http.HttpProtocol.IsHttp09(string! protocol) -> bool
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Http
7+
{
8+
/// <summary>
9+
/// The result of creating a <see cref="RequestDelegate" /> from a <see cref="Delegate" />
10+
/// </summary>
11+
public sealed class RequestDelegateResult
12+
{
13+
/// <summary>
14+
/// Creates a new instance of <see cref="RequestDelegateResult"/>.
15+
/// </summary>
16+
public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList<object> metadata)
17+
{
18+
RequestDelegate = requestDelegate;
19+
EndpointMetadata = metadata;
20+
}
21+
22+
/// <summary>
23+
/// Gets the <see cref="RequestDelegate" />
24+
/// </summary>
25+
public RequestDelegate RequestDelegate { get;}
26+
27+
/// <summary>
28+
/// Gets endpoint metadata inferred from creating the <see cref="RequestDelegate" />
29+
/// </summary>
30+
public IReadOnlyList<object> EndpointMetadata { get;}
31+
}
32+
33+
}

src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

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

2122
<ItemGroup>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList<T>(th
192192
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders!
193193
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders!
194194
static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string?
195-
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate!
196-
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!
195+
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegateResult!
196+
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!
197197
static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void
198198
static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void
199199
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!

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

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using System.Linq;
66
using System.Linq.Expressions;
7+
using System.Net.Http;
78
using System.Reflection;
89
using System.Security.Claims;
910
using System.Text;
@@ -63,14 +64,16 @@ public static partial class RequestDelegateFactory
6364
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
6465
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
6566

67+
private static readonly AcceptsMetadata DefaultAcceptsMetadata = new(new[] { "application/json" });
68+
6669
/// <summary>
6770
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
6871
/// </summary>
6972
/// <param name="action">A request handler with any number of custom parameters that often produces a response with its return value.</param>
7073
/// <param name="options">The <see cref="RequestDelegateFactoryOptions"/> used to configure the behavior of the handler.</param>
71-
/// <returns>The <see cref="RequestDelegate"/>.</returns>
74+
/// <returns>The <see cref="RequestDelegateResult"/>.</returns>
7275
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
73-
public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOptions? options = null)
76+
public static RequestDelegateResult Create(Delegate action, RequestDelegateFactoryOptions? options = null)
7477
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
7578
{
7679
if (action is null)
@@ -84,12 +87,15 @@ public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOpti
8487
null => null,
8588
};
8689

87-
var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, targetExpression);
88-
89-
return httpContext =>
90+
var factoryContext = new FactoryContext
9091
{
91-
return targetableRequestDelegate(action.Target, httpContext);
92+
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
9293
};
94+
95+
var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, factoryContext, targetExpression);
96+
97+
return new RequestDelegateResult(httpContext => targetableRequestDelegate(action.Target, httpContext), factoryContext.Metadata);
98+
9399
}
94100

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

122+
var factoryContext = new FactoryContext
123+
{
124+
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
125+
};
126+
116127
if (targetFactory is null)
117128
{
118129
if (methodInfo.IsStatic)
119130
{
120-
var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression: null);
131+
var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression: null);
121132

122-
return httpContext =>
123-
{
124-
return untargetableRequestDelegate(null, httpContext);
125-
};
133+
return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata);
126134
}
127135

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

131139
var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType);
132-
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression);
140+
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression);
133141

134-
return httpContext =>
135-
{
136-
return targetableRequestDelegate(targetFactory(httpContext), httpContext);
137-
};
142+
return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata);
138143
}
139144

140-
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, Expression? targetExpression)
145+
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression)
141146
{
142147
// Non void return type
143148

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

158-
var factoryContext = new FactoryContext()
159-
{
160-
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
161-
};
162-
163163
if (options?.RouteParameterNames is { } routeParameterNames)
164164
{
165165
factoryContext.RouteParameters = new(routeParameterNames);
@@ -861,6 +861,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
861861
}
862862
}
863863

864+
factoryContext.Metadata.Add(DefaultAcceptsMetadata);
864865
var isOptional = IsOptionalParameter(parameter);
865866

866867
factoryContext.JsonRequestBodyType = parameter.ParameterType;
@@ -1111,6 +1112,8 @@ private class FactoryContext
11111112

11121113
public Dictionary<string, string> TrackedParameters { get; } = new();
11131114
public bool HasMultipleBodyParameters { get; set; }
1115+
1116+
public List<object> Metadata { get; } = new();
11141117
}
11151118

11161119
private static class RequestDelegateFactoryConstants

0 commit comments

Comments
 (0)