Skip to content

Commit

Permalink
Added support for binding the raw request body (#39388)
Browse files Browse the repository at this point in the history
- Added support for Stream, and PipeReader
- Added tests
  • Loading branch information
davidfowl authored Jan 20, 2022
1 parent bc1efeb commit 08d1b8a
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Globalization;
using System.IO.Pipelines;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -61,6 +62,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));
Expand Down Expand Up @@ -302,6 +305,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);
Expand Down
127 changes: 127 additions & 0 deletions src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using System.Buffers;
using System.Globalization;
using System.IO.Pipelines;
using System.Linq.Expressions;
Expand Down Expand Up @@ -1376,6 +1377,132 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action)
Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name);
}

public static object[][] RawFromBodyActions
{
get
{
void TestStream(HttpContext httpContext, Stream stream)
{
var ms = new MemoryStream();
stream.CopyTo(ms);
httpContext.Items.Add("body", ms.ToArray());
}

async Task TestPipeReader(HttpContext httpContext, PipeReader reader)
{
var ms = new MemoryStream();
await reader.CopyToAsync(ms);
httpContext.Items.Add("body", ms.ToArray());
}

return new[]
{
new object[] { (Action<HttpContext, Stream>)TestStream },
new object[] { (Func<HttpContext, PipeReader, Task>)TestPipeReader }
};
}
}

[Theory]
[MemberData(nameof(RawFromBodyActions))]
public async Task RequestDelegatePopulatesFromRawBodyParameter(Delegate action)
{
var httpContext = CreateHttpContext();

var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new
{
Name = "Write more tests!"
});

var stream = new MemoryStream(requestBodyBytes);
httpContext.Request.Body = stream;

httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture);
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));

var mock = new Mock<IServiceProvider>();
httpContext.RequestServices = mock.Object;

var factoryResult = RequestDelegateFactory.Create(action);

var requestDelegate = factoryResult.RequestDelegate;

await requestDelegate(httpContext);

Assert.Same(httpContext.Request.Body, stream);

// Assert that we can read the body from both the pipe reader and Stream after executing
httpContext.Request.Body.Position = 0;
byte[] data = new byte[requestBodyBytes.Length];
int read = await httpContext.Request.Body.ReadAsync(data.AsMemory());
Assert.Equal(read, data.Length);
Assert.Equal(requestBodyBytes, data);

httpContext.Request.Body.Position = 0;
var result = await httpContext.Request.BodyReader.ReadAsync();
Assert.Equal(requestBodyBytes.Length, result.Buffer.Length);
Assert.Equal(requestBodyBytes, result.Buffer.ToArray());
httpContext.Request.BodyReader.AdvanceTo(result.Buffer.End);

var rawRequestBody = httpContext.Items["body"];
Assert.NotNull(rawRequestBody);
Assert.Equal(requestBodyBytes, (byte[])rawRequestBody!);
}

[Theory]
[MemberData(nameof(RawFromBodyActions))]
public async Task RequestDelegatePopulatesFromRawBodyParameterPipeReader(Delegate action)
{
var httpContext = CreateHttpContext();

var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new
{
Name = "Write more tests!"
});

var pipeReader = PipeReader.Create(new MemoryStream(requestBodyBytes));
var stream = pipeReader.AsStream();
httpContext.Features.Set<IRequestBodyPipeFeature>(new PipeRequestBodyFeature(pipeReader));
httpContext.Request.Body = stream;

httpContext.Request.Headers["Content-Length"] = requestBodyBytes.Length.ToString(CultureInfo.InvariantCulture);
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));

var mock = new Mock<IServiceProvider>();
httpContext.RequestServices = mock.Object;

var factoryResult = RequestDelegateFactory.Create(action);

var requestDelegate = factoryResult.RequestDelegate;

await requestDelegate(httpContext);

Assert.Same(httpContext.Request.Body, stream);
Assert.Same(httpContext.Request.BodyReader, pipeReader);

// Assert that we can read the body from both the pipe reader and Stream after executing and verify that they are empty (the pipe reader isn't seekable here)
int read = await httpContext.Request.Body.ReadAsync(new byte[requestBodyBytes.Length].AsMemory());
Assert.Equal(0, read);

var result = await httpContext.Request.BodyReader.ReadAsync();
Assert.Equal(0, result.Buffer.Length);
Assert.True(result.IsCompleted);
httpContext.Request.BodyReader.AdvanceTo(result.Buffer.End);

var rawRequestBody = httpContext.Items["body"];
Assert.NotNull(rawRequestBody);
Assert.Equal(requestBodyBytes, (byte[])rawRequestBody!);
}

class PipeRequestBodyFeature : IRequestBodyPipeFeature
{
public PipeRequestBodyFeature(PipeReader pipeReader)
{
Reader = pipeReader;
}
public PipeReader Reader { get; set; }
}

[Theory]
[MemberData(nameof(ExplicitFromBodyActions))]
public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter(Delegate action)
Expand Down

0 comments on commit 08d1b8a

Please sign in to comment.