Skip to content

Commit 08d1b8a

Browse files
authored
Added support for binding the raw request body (#39388)
- Added support for Stream, and PipeReader - Added tests
1 parent bc1efeb commit 08d1b8a

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Globalization;
6+
using System.IO.Pipelines;
67
using System.Linq;
78
using System.Linq.Expressions;
89
using System.Reflection;
@@ -61,6 +62,8 @@ public static partial class RequestDelegateFactory
6162
private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Query))!);
6263
private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Headers))!);
6364
private static readonly MemberExpression FormExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Form))!);
65+
private static readonly MemberExpression RequestStreamExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Body))!);
66+
private static readonly MemberExpression RequestPipeReaderExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.BodyReader))!);
6467
private static readonly MemberExpression FormFilesExpr = Expression.Property(FormExpr, typeof(IFormCollection).GetProperty(nameof(IFormCollection.Files))!);
6568
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
6669
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
@@ -302,6 +305,14 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
302305
{
303306
return BindParameterFromFormFile(parameter, parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileParameter);
304307
}
308+
else if (parameter.ParameterType == typeof(Stream))
309+
{
310+
return RequestStreamExpr;
311+
}
312+
else if (parameter.ParameterType == typeof(PipeReader))
313+
{
314+
return RequestPipeReaderExpr;
315+
}
305316
else if (ParameterBindingMethodCache.HasBindAsyncMethod(parameter))
306317
{
307318
return BindParameterFromBindAsync(parameter, factoryContext);

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#nullable enable
55

6+
using System.Buffers;
67
using System.Globalization;
78
using System.IO.Pipelines;
89
using System.Linq.Expressions;
@@ -1376,6 +1377,132 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action)
13761377
Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name);
13771378
}
13781379

1380+
public static object[][] RawFromBodyActions
1381+
{
1382+
get
1383+
{
1384+
void TestStream(HttpContext httpContext, Stream stream)
1385+
{
1386+
var ms = new MemoryStream();
1387+
stream.CopyTo(ms);
1388+
httpContext.Items.Add("body", ms.ToArray());
1389+
}
1390+
1391+
async Task TestPipeReader(HttpContext httpContext, PipeReader reader)
1392+
{
1393+
var ms = new MemoryStream();
1394+
await reader.CopyToAsync(ms);
1395+
httpContext.Items.Add("body", ms.ToArray());
1396+
}
1397+
1398+
return new[]
1399+
{
1400+
new object[] { (Action<HttpContext, Stream>)TestStream },
1401+
new object[] { (Func<HttpContext, PipeReader, Task>)TestPipeReader }
1402+
};
1403+
}
1404+
}
1405+
1406+
[Theory]
1407+
[MemberData(nameof(RawFromBodyActions))]
1408+
public async Task RequestDelegatePopulatesFromRawBodyParameter(Delegate action)
1409+
{
1410+
var httpContext = CreateHttpContext();
1411+
1412+
var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new
1413+
{
1414+
Name = "Write more tests!"
1415+
});
1416+
1417+
var stream = new MemoryStream(requestBodyBytes);
1418+
httpContext.Request.Body = stream;
1419+
1420+
httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture);
1421+
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
1422+
1423+
var mock = new Mock<IServiceProvider>();
1424+
httpContext.RequestServices = mock.Object;
1425+
1426+
var factoryResult = RequestDelegateFactory.Create(action);
1427+
1428+
var requestDelegate = factoryResult.RequestDelegate;
1429+
1430+
await requestDelegate(httpContext);
1431+
1432+
Assert.Same(httpContext.Request.Body, stream);
1433+
1434+
// Assert that we can read the body from both the pipe reader and Stream after executing
1435+
httpContext.Request.Body.Position = 0;
1436+
byte[] data = new byte[requestBodyBytes.Length];
1437+
int read = await httpContext.Request.Body.ReadAsync(data.AsMemory());
1438+
Assert.Equal(read, data.Length);
1439+
Assert.Equal(requestBodyBytes, data);
1440+
1441+
httpContext.Request.Body.Position = 0;
1442+
var result = await httpContext.Request.BodyReader.ReadAsync();
1443+
Assert.Equal(requestBodyBytes.Length, result.Buffer.Length);
1444+
Assert.Equal(requestBodyBytes, result.Buffer.ToArray());
1445+
httpContext.Request.BodyReader.AdvanceTo(result.Buffer.End);
1446+
1447+
var rawRequestBody = httpContext.Items["body"];
1448+
Assert.NotNull(rawRequestBody);
1449+
Assert.Equal(requestBodyBytes, (byte[])rawRequestBody!);
1450+
}
1451+
1452+
[Theory]
1453+
[MemberData(nameof(RawFromBodyActions))]
1454+
public async Task RequestDelegatePopulatesFromRawBodyParameterPipeReader(Delegate action)
1455+
{
1456+
var httpContext = CreateHttpContext();
1457+
1458+
var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(new
1459+
{
1460+
Name = "Write more tests!"
1461+
});
1462+
1463+
var pipeReader = PipeReader.Create(new MemoryStream(requestBodyBytes));
1464+
var stream = pipeReader.AsStream();
1465+
httpContext.Features.Set<IRequestBodyPipeFeature>(new PipeRequestBodyFeature(pipeReader));
1466+
httpContext.Request.Body = stream;
1467+
1468+
httpContext.Request.Headers["Content-Length"] = requestBodyBytes.Length.ToString(CultureInfo.InvariantCulture);
1469+
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
1470+
1471+
var mock = new Mock<IServiceProvider>();
1472+
httpContext.RequestServices = mock.Object;
1473+
1474+
var factoryResult = RequestDelegateFactory.Create(action);
1475+
1476+
var requestDelegate = factoryResult.RequestDelegate;
1477+
1478+
await requestDelegate(httpContext);
1479+
1480+
Assert.Same(httpContext.Request.Body, stream);
1481+
Assert.Same(httpContext.Request.BodyReader, pipeReader);
1482+
1483+
// 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)
1484+
int read = await httpContext.Request.Body.ReadAsync(new byte[requestBodyBytes.Length].AsMemory());
1485+
Assert.Equal(0, read);
1486+
1487+
var result = await httpContext.Request.BodyReader.ReadAsync();
1488+
Assert.Equal(0, result.Buffer.Length);
1489+
Assert.True(result.IsCompleted);
1490+
httpContext.Request.BodyReader.AdvanceTo(result.Buffer.End);
1491+
1492+
var rawRequestBody = httpContext.Items["body"];
1493+
Assert.NotNull(rawRequestBody);
1494+
Assert.Equal(requestBodyBytes, (byte[])rawRequestBody!);
1495+
}
1496+
1497+
class PipeRequestBodyFeature : IRequestBodyPipeFeature
1498+
{
1499+
public PipeRequestBodyFeature(PipeReader pipeReader)
1500+
{
1501+
Reader = pipeReader;
1502+
}
1503+
public PipeReader Reader { get; set; }
1504+
}
1505+
13791506
[Theory]
13801507
[MemberData(nameof(ExplicitFromBodyActions))]
13811508
public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter(Delegate action)

0 commit comments

Comments
 (0)