From 08d1b8a2ecc6f41e8c483eab9567a8690f1c07a1 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 20 Jan 2022 10:05:39 -0800 Subject: [PATCH] Added support for binding the raw request body (#39388) - Added support for Stream, and PipeReader - Added tests --- .../src/RequestDelegateFactory.cs | 11 ++ .../test/RequestDelegateFactoryTests.cs | 127 ++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index e97d75b5e453..62f87635face 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Globalization; +using System.IO.Pipelines; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -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>(() => Task.CompletedTask)); @@ -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); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index b659ba4c152c..ef22b01c11a0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Buffers; using System.Globalization; using System.IO.Pipelines; using System.Linq.Expressions; @@ -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)TestStream }, + new object[] { (Func)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(new RequestBodyDetectionFeature(true)); + + var mock = new Mock(); + 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(new PipeRequestBodyFeature(pipeReader)); + httpContext.Request.Body = stream; + + httpContext.Request.Headers["Content-Length"] = requestBodyBytes.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var mock = new Mock(); + 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)