-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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
Added support for binding the raw request body #39388
Changes from 12 commits
daf6a86
154abd1
d08f132
3e2fff8
547ce9b
c7d1168
8a17a3b
34244b1
7c9b45e
d2085fd
f45db2b
a0711dc
caf7d66
df767a7
191a9cf
a6637e0
39152b3
0210b28
f5c305a
5b03d54
c1f4063
1cd9735
27c3f48
9a7f593
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Buffers; | ||
using System.Diagnostics; | ||
using System.Globalization; | ||
using System.IO.Pipelines; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
|
@@ -48,6 +50,8 @@ public static partial class RequestDelegateFactory | |
|
||
private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target"); | ||
private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue"); | ||
private static readonly ParameterExpression RawBodyValueExpr = Expression.Parameter(typeof(ReadOnlySequence<byte>), "bodyValue"); | ||
private static readonly MemberExpression RawBodyIsEmptyExpr = Expression.Property(RawBodyValueExpr, typeof(ReadOnlySequence<byte>).GetProperty(nameof(ReadOnlySequence<byte>.IsEmpty))!); | ||
private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure"); | ||
private static readonly ParameterExpression BoundValuesArrayExpr = Expression.Parameter(typeof(object[]), "boundValues"); | ||
|
||
|
@@ -61,6 +65,8 @@ public static partial class RequestDelegateFactory | |
private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Query))!); | ||
private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Headers))!); | ||
private static readonly MemberExpression FormExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Form))!); | ||
private static readonly MemberExpression RequestStreamExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Body))!); | ||
private static readonly MemberExpression RequestPipeReaderExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.BodyReader))!); | ||
private static readonly MemberExpression FormFilesExpr = Expression.Property(FormExpr, typeof(IFormCollection).GetProperty(nameof(IFormCollection.Files))!); | ||
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!); | ||
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask)); | ||
|
@@ -199,10 +205,10 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory | |
var errorMessage = BuildErrorMessageForInferredBodyParameter(factoryContext); | ||
throw new InvalidOperationException(errorMessage); | ||
} | ||
if (factoryContext.JsonRequestBodyParameter is not null && | ||
if (factoryContext.RequestBodyParameter is not null && | ||
factoryContext.FirstFormRequestBodyParameter is not null) | ||
{ | ||
var errorMessage = BuildErrorMessageForFormAndJsonBodyParameters(factoryContext); | ||
var errorMessage = BuildErrorMessageForFormAndBodyParameters(factoryContext); | ||
throw new InvalidOperationException(errorMessage); | ||
} | ||
if (factoryContext.HasMultipleBodyParameters) | ||
|
@@ -302,6 +308,14 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext | |
{ | ||
return BindParameterFromFormFile(parameter, parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileParameter); | ||
} | ||
else if (parameter.ParameterType == typeof(Stream)) | ||
{ | ||
return RequestStreamExpr; | ||
} | ||
else if (parameter.ParameterType == typeof(PipeReader)) | ||
{ | ||
return RequestPipeReaderExpr; | ||
} | ||
else if (ParameterBindingMethodCache.HasBindAsyncMethod(parameter)) | ||
{ | ||
return BindParameterFromBindAsync(parameter, factoryContext); | ||
|
@@ -549,7 +563,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, | |
|
||
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) | ||
{ | ||
if (factoryContext.JsonRequestBodyParameter is null && !factoryContext.ReadForm) | ||
if (factoryContext.RequestBodyParameter is null && !factoryContext.ReadForm) | ||
{ | ||
if (factoryContext.ParameterBinders.Count > 0) | ||
{ | ||
|
@@ -582,19 +596,102 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, | |
{ | ||
return HandleRequestBodyAndCompileRequestDelegateForForm(responseWritingMethodCall, factoryContext); | ||
} | ||
else if (factoryContext.IsRawRequestBody) | ||
{ | ||
return HandleRequestBodyAndCompileRequestDelegateForRawBody(responseWritingMethodCall, factoryContext); | ||
} | ||
|
||
return HandleRequestBodyAndCompileRequestDelegateForJsonBody(responseWritingMethodCall, factoryContext); | ||
} | ||
|
||
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegateForRawBody(Expression responseWritingMethodCall, FactoryContext factoryContext) | ||
{ | ||
Debug.Assert(factoryContext.RequestBodyParameter is not null, "factoryContext.RequestBodyParameter is null for a body parameter."); | ||
Debug.Assert(factoryContext.RequestBodyParameter.Name is not null, "CreateArgument() should throw if parameter.Name is null."); | ||
|
||
if (factoryContext.ParameterBinders.Count > 0) | ||
{ | ||
// We need to generate the code for reading from the body before calling into the delegate | ||
var continuation = Expression.Lambda<Func<object?, HttpContext, ReadOnlySequence<byte>, object?[], Task>>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noob question - why is this task returning (vs ValueTask returning)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be either but using Task vs ValueTask but there’s no benefit to using ValueTask here. There’s no pooling to be had and synchronous completion won’t allocate regardless |
||
responseWritingMethodCall, TargetExpr, HttpContextExpr, RawBodyValueExpr, BoundValuesArrayExpr).Compile(); | ||
|
||
// Looping over arrays is faster | ||
var binders = factoryContext.ParameterBinders.ToArray(); | ||
var count = binders.Length; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could capture this inside the lamda, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally didn’t do it would only capture these locals only (in case we nulled out the factory context later). |
||
|
||
return async (target, httpContext) => | ||
{ | ||
// Run these first so that they can potentially read and rewind the body | ||
var boundValues = new object?[count]; | ||
|
||
for (var i = 0; i < count; i++) | ||
{ | ||
boundValues[i] = await binders[i](httpContext); | ||
} | ||
|
||
var (bodyValue, successful) = await TryReadBodyAsync(httpContext); | ||
|
||
if (!successful) | ||
{ | ||
return; | ||
} | ||
|
||
await continuation(target, httpContext, bodyValue, boundValues); | ||
}; | ||
} | ||
else | ||
{ | ||
return HandleRequestBodyAndCompileRequestDelegateForJson(responseWritingMethodCall, factoryContext); | ||
// We need to generate the code for reading from the body before calling into the delegate | ||
var continuation = Expression.Lambda<Func<object?, HttpContext, ReadOnlySequence<byte>, Task>>( | ||
responseWritingMethodCall, TargetExpr, HttpContextExpr, RawBodyValueExpr).Compile(); | ||
|
||
return async (target, httpContext) => | ||
{ | ||
var (bodyValue, successful) = await TryReadBodyAsync(httpContext); | ||
|
||
if (!successful) | ||
{ | ||
return; | ||
} | ||
|
||
await continuation(target, httpContext, bodyValue); | ||
}; | ||
} | ||
|
||
static async Task<(ReadOnlySequence<byte>, bool)> TryReadBodyAsync(HttpContext httpContext) | ||
davidfowl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>(); | ||
|
||
if (feature?.CanHaveBody == true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the behavior with JSON if there isn't a body to read from? We should try and emulate that behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If allow empty body is true it’ll bind otherwise 400 |
||
{ | ||
var bodyReader = httpContext.Request.BodyReader; | ||
|
||
while (true) | ||
{ | ||
var result = await bodyReader.ReadAsync(); | ||
var buffer = result.Buffer; | ||
|
||
if (result.IsCompleted) | ||
{ | ||
return (buffer, true); | ||
} | ||
|
||
// Buffer the body | ||
bodyReader.AdvanceTo(buffer.Start, buffer.End); | ||
} | ||
} | ||
|
||
return (default, true); | ||
davidfowl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegateForJson(Expression responseWritingMethodCall, FactoryContext factoryContext) | ||
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegateForJsonBody(Expression responseWritingMethodCall, FactoryContext factoryContext) | ||
{ | ||
Debug.Assert(factoryContext.JsonRequestBodyParameter is not null, "factoryContext.JsonRequestBodyParameter is null for a JSON body."); | ||
Debug.Assert(factoryContext.RequestBodyParameter is not null, "factoryContext.RequestBodyParameter is null for a body parameter."); | ||
|
||
var bodyType = factoryContext.JsonRequestBodyParameter.ParameterType; | ||
var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.JsonRequestBodyParameter.ParameterType, fullName: false); | ||
var parameterName = factoryContext.JsonRequestBodyParameter.Name; | ||
var bodyType = factoryContext.RequestBodyParameter.ParameterType; | ||
var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.RequestBodyParameter.ParameterType, fullName: false); | ||
var parameterName = factoryContext.RequestBodyParameter.Name; | ||
|
||
Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); | ||
|
||
|
@@ -1157,7 +1254,7 @@ private static Expression BindParameterFromFormFile( | |
|
||
private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) | ||
{ | ||
if (factoryContext.JsonRequestBodyParameter is not null) | ||
if (factoryContext.RequestBodyParameter is not null) | ||
{ | ||
factoryContext.HasMultipleBodyParameters = true; | ||
var parameterName = parameter.Name; | ||
|
@@ -1173,9 +1270,14 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al | |
|
||
var isOptional = IsOptionalParameter(parameter, factoryContext); | ||
|
||
factoryContext.JsonRequestBodyParameter = parameter; | ||
factoryContext.RequestBodyParameter = parameter; | ||
factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional; | ||
factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType)); | ||
|
||
// We can't infer the accept content type for raw bodies | ||
if (!factoryContext.IsRawRequestBody) | ||
{ | ||
factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType)); | ||
} | ||
|
||
if (!factoryContext.AllowEmptyRequestBody) | ||
{ | ||
|
@@ -1188,6 +1290,8 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al | |
// } | ||
factoryContext.ParamCheckExpressions.Add(Expression.Block( | ||
Expression.IfThen( | ||
factoryContext.IsRawRequestBody ? | ||
RawBodyIsEmptyExpr : | ||
Expression.Equal(BodyValueExpr, Expression.Constant(null)), | ||
Expression.Block( | ||
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), | ||
|
@@ -1212,7 +1316,9 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al | |
// } | ||
var checkRequiredBodyBlock = Expression.Block( | ||
Expression.IfThen( | ||
Expression.Equal(BodyValueExpr, Expression.Constant(null)), | ||
factoryContext.IsRawRequestBody ? | ||
RawBodyIsEmptyExpr : | ||
Expression.Equal(BodyValueExpr, Expression.Constant(null)), | ||
Expression.Block( | ||
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), | ||
Expression.Call(LogRequiredParameterNotProvidedMethod, | ||
|
@@ -1230,12 +1336,22 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al | |
} | ||
else if (parameter.HasDefaultValue) | ||
{ | ||
if (factoryContext.IsRawRequestBody) | ||
{ | ||
return Expression.Coalesce(RawBodyValueExpr, Expression.Constant(parameter.DefaultValue)); | ||
} | ||
|
||
// Convert(bodyValue ?? SomeDefault, Todo) | ||
return Expression.Convert( | ||
Expression.Coalesce(BodyValueExpr, Expression.Constant(parameter.DefaultValue)), | ||
parameter.ParameterType); | ||
} | ||
|
||
if (factoryContext.IsRawRequestBody) | ||
{ | ||
return RawBodyValueExpr; | ||
} | ||
|
||
// Convert(bodyValue, Todo) | ||
return Expression.Convert(BodyValueExpr, parameter.ParameterType); | ||
} | ||
|
@@ -1445,8 +1561,9 @@ private class FactoryContext | |
public bool DisableInferredFromBody { get; init; } | ||
|
||
// Temporary State | ||
public ParameterInfo? JsonRequestBodyParameter { get; set; } | ||
public ParameterInfo? RequestBodyParameter { get; set; } | ||
public bool AllowEmptyRequestBody { get; set; } | ||
public bool IsRawRequestBody => RequestBodyParameter?.ParameterType == typeof(ReadOnlySequence<byte>); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be interesting to see if people actually start using it. It's a somewhat obtuse type to actually use (particularly when you can bind the PipeReader instead). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It’s something we’d have to advertise. I had support for ReadOnlyMemory<byte> and byte[] but removed them. |
||
|
||
public bool UsingTempSourceString { get; set; } | ||
public List<ParameterExpression> ExtraLocals { get; } = new(); | ||
|
@@ -1682,10 +1799,10 @@ private static string BuildErrorMessageForInferredBodyParameter(FactoryContext f | |
return errorMessage.ToString(); | ||
} | ||
|
||
private static string BuildErrorMessageForFormAndJsonBodyParameters(FactoryContext factoryContext) | ||
private static string BuildErrorMessageForFormAndBodyParameters(FactoryContext factoryContext) | ||
{ | ||
var errorMessage = new StringBuilder(); | ||
errorMessage.AppendLine("An action cannot use both form and JSON body parameters."); | ||
errorMessage.AppendLine("An action cannot use both form and body parameters."); | ||
errorMessage.AppendLine("Below is the list of parameters that we found: "); | ||
errorMessage.AppendLine(); | ||
errorMessage.AppendLine(FormattableString.Invariant($"{"Parameter",-20}| {"Source",-30}")); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think so. We're asserting that it isn't null