diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 2ceedae2c3d5..ac0ee789c232 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -56,7 +56,7 @@ internal static class RequestDelegateGeneratorSources httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } - return (false, default); + return (allowEmpty, default); } """; diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 9cb2e4ec7a51..5aaab275a4d1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -1497,249 +1497,6 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name); } - public static object[][] ImplicitRawFromBodyActions - { - 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 } - }; - } - } - - public static object[][] ExplicitRawFromBodyActions - { - get - { - void TestStream(HttpContext httpContext, [FromBody] Stream stream) - { - var ms = new MemoryStream(); - stream.CopyTo(ms); - httpContext.Items.Add("body", ms.ToArray()); - } - - async Task TestPipeReader(HttpContext httpContext, [FromBody] 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(ImplicitRawFromBodyActions))] - public async Task RequestDelegatePopulatesFromImplicitRawBodyParameter(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(ExplicitRawFromBodyActions))] - public async Task RequestDelegatePopulatesFromExplicitRawBodyParameter(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(ImplicitRawFromBodyActions))] - public async Task RequestDelegatePopulatesFromImplicitRawBodyParameterPipeReader(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!); - } - - [Theory] - [MemberData(nameof(ExplicitRawFromBodyActions))] - public async Task RequestDelegatePopulatesFromExplicitRawBodyParameterPipeReader(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) @@ -1778,31 +1535,6 @@ public async Task RequestDelegateRejectsEmptyBodyGivenImplicitFromBodyParameter( Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", ex.Message); } - [Fact] - public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectyConfiguredFromBodyParameter() - { - var structToBeZeroed = new BodyStruct - { - Id = 42 - }; - - void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) - { - structToBeZeroed = bodyStruct; - } - - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; - - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(default, structToBeZeroed); - } - [Fact] public void RequestDelegateFactoryThrowsForByRefReturnTypes() { @@ -7131,11 +6863,6 @@ public override void Write(Utf8JsonWriter writer, ITodo value, JsonSerializerOpt } } - private struct BodyStruct - { - public int Id { get; set; } - } - private class FromRouteAttribute : Attribute, IFromRouteMetadata { public string? Name { get; set; } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt index af8de12dad7c..d52413407ed1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt @@ -312,7 +312,7 @@ namespace Microsoft.AspNetCore.Http.Generated httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } - return (false, default); + return (allowEmpty, default); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt index ab124d304b16..e41b5347c728 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Http.Generated httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } - return (false, default); + return (allowEmpty, default); } private static Func> ResolveJsonBodyOrService(IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index f7b9ef8dfa04..6c19d58a2169 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -246,7 +246,7 @@ namespace Microsoft.AspNetCore.Http.Generated httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } - return (false, default); + return (allowEmpty, default); } private static Func> ResolveJsonBodyOrService(IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt index dc6304f0692a..cd0d311f62f3 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt @@ -249,7 +249,7 @@ namespace Microsoft.AspNetCore.Http.Generated httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } - return (false, default); + return (allowEmpty, default); } private static Func> ResolveJsonBodyOrService(IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs index a42f048a8a9c..8037a9647bb1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.IO.Pipelines; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; @@ -433,4 +434,13 @@ public RequestBodyDetectionFeature(bool canHaveBody) public bool CanHaveBody { get; } } + + internal sealed class PipeRequestBodyFeature : IRequestBodyPipeFeature + { + public PipeRequestBodyFeature(PipeReader pipeReader) + { + Reader = pipeReader; + } + public PipeReader Reader { get; set; } + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs index fae532604529..0ce47287e0af 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBody.cs @@ -1,6 +1,8 @@ // 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.Globalization; +using System.IO.Pipelines; using System.Text.Json; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Http.Generators.Tests; @@ -148,4 +150,255 @@ public async Task MapAction_ExplicitBodyParam_ComplexReturn_Snapshot() await endpoints[1].RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, string.Empty); } + + public static object[][] ImplicitRawFromBodyActions + { + get + { + var testStreamSource = """ +void TestStream(HttpContext httpContext, System.IO.Stream stream) +{ + var ms = new System.IO.MemoryStream(); + stream.CopyTo(ms); + httpContext.Items.Add("body", ms.ToArray()); +} +app.MapPost("/", TestStream); +"""; + var testPipeReaderSource = """ +async Task TestPipeReader(HttpContext httpContext, System.IO.Pipelines.PipeReader reader) +{ + var ms = new System.IO.MemoryStream(); + await reader.CopyToAsync(ms); + httpContext.Items.Add("body", ms.ToArray()); +} +app.MapPost("/", TestPipeReader); +"""; + + return new[] + { + new object[] { testStreamSource }, + new object[] { testPipeReaderSource } + }; + } + } + + public static object[][] ExplicitRawFromBodyActions + { + get + { + var explicitTestStreamSource = """ +void TestStream(HttpContext httpContext, [FromBody] System.IO.Stream stream) +{ + var ms = new System.IO.MemoryStream(); + stream.CopyTo(ms); + httpContext.Items.Add("body", ms.ToArray()); +} +app.MapPost("/", TestStream); +"""; + + var explicitTestPipeReaderSource = """ +async Task TestPipeReader(HttpContext httpContext, [FromBody] System.IO.Pipelines.PipeReader reader) +{ + var ms = new System.IO.MemoryStream(); + await reader.CopyToAsync(ms); + httpContext.Items.Add("body", ms.ToArray()); +} +app.MapPost("/", TestPipeReader); +"""; + + return new[] + { + new object[] { explicitTestStreamSource }, + new object[] { explicitTestPipeReaderSource } + }; + } + } + + [Theory] + [MemberData(nameof(ImplicitRawFromBodyActions))] + public async Task RequestDelegatePopulatesFromImplicitRawBodyParameter(string source) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + 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)); + + await endpoint.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(ExplicitRawFromBodyActions))] + public async Task RequestDelegatePopulatesFromExplicitRawBodyParameter(string source) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + 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)); + + await endpoint.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(ImplicitRawFromBodyActions))] + public async Task RequestDelegatePopulatesFromImplicitRawBodyParameterPipeReader(string source) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + 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)); + + await endpoint.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!); + } + + [Theory] + [MemberData(nameof(ExplicitRawFromBodyActions))] + public async Task RequestDelegatePopulatesFromExplicitRawBodyParameterPipeReader(string source) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + 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)); + + await endpoint.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!); + } + + [Fact] + public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectlyConfiguredFromBodyParameter() + { + var structToBeZeroedKey = "structToBeZeroed"; + + var source = $$""" +void TestAction(HttpContext httpContext, [CustomFromBody(AllowEmpty = true)] BodyStruct bodyStruct) +{ + httpContext.Items["{{structToBeZeroedKey}}"] = bodyStruct; +} +app.MapPost("/", TestAction); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + httpContext.Items[structToBeZeroedKey] = new BodyStruct { Id = 42 }; + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(default(BodyStruct), httpContext.Items[structToBeZeroedKey]); + } }