diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index d9f553b53d7fe12a1e8392c664ef84209ade1259..a40ca4a8564e815b723b07219d8ea84529fcab97 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs @@ -77,8 +77,6 @@ namespace Microsoft.AspNetCore.WebUtilities [System.Diagnostics.DebuggerStepThroughAttribute] public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Pipelines.PipeWriter destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Stream destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override void Flush() { } public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } diff --git a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs index be60ebe9945021eaf33332598e107fb72c80e4e9..21ce9e2af03247b27c2eb31e47ed64867852bcfb 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -186,25 +186,8 @@ namespace Microsoft.AspNetCore.WebUtilities // unspooled content. Copy the FileStream content first when available. if (FileStream != null) { - // We make a new stream for async reads from disk and async writes to the destination - await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); - - await readStream.CopyToAsync(destination, cancellationToken); - - // This is created with delete on close - await FileStream.DisposeAsync(); - FileStream = null; - } + await FileStream.FlushAsync(cancellationToken); - await PagedByteBuffer.MoveToAsync(destination, cancellationToken); - } - - public async Task DrainBufferAsync(PipeWriter destination, CancellationToken cancellationToken = default) - { - // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer" - // unspooled content. Copy the FileStream content first when available. - if (FileStream != null) - { // We make a new stream for async reads from disk and async writes to the destination await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); diff --git a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs index 007252fb49802599fb3d5ebe7d58609e7a3713d7..bb98a7eac2a63cdaef9de3163bf65afcac4ea592 100644 --- a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs @@ -371,6 +371,24 @@ namespace Microsoft.AspNetCore.WebUtilities Assert.Equal(0, bufferingStream.Length); } + [Fact] + public async Task DrainBufferAsync_IncludesContentPossiblyBufferedByFileStream() + { + // We want to ensure that the FileStream (which has a 1-byte buffer) flushes prior to the other read stream reading input. + // Arrange + var input = new byte[] { 3, }; + using var bufferingStream = new FileBufferingWriteStream(0, tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, input.Length); + var memoryStream = new MemoryStream(); + + // Act + await bufferingStream.DrainBufferAsync(memoryStream, default); + + // Assert + Assert.Equal(input, memoryStream.ToArray()); + Assert.Equal(0, bufferingStream.Length); + } + public void Dispose() { try diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs index c0d929ecea6db9f0d4a6589f708ece7265acc05a..e6243fcb4559a3b1535ef60d5e4b7e9eb5f7596d 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters if (fileBufferingWriteStream != null) { response.ContentLength = fileBufferingWriteStream.Length; - await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter); + await fileBufferingWriteStream.DrainBufferAsync(response.Body); } } finally diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs index 73dc60e49d3189858a19e4ac99d3bdb3036ab8e6..b6889c1c2d8780656a36a80a8b1f441e63bd41aa 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs @@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters if (fileBufferingWriteStream != null) { response.ContentLength = fileBufferingWriteStream.Length; - await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter); + await fileBufferingWriteStream.DrainBufferAsync(response.Body); } } finally diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs index c5c4ae51fdff4efb4fac8c3ab92679f74f4482ae..40ea303614ba89047942a39bbb0d0fb977ebc38e 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -154,15 +154,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(expected, await response.Content.ReadAsStringAsync()); } - [Fact] - public virtual async Task Formatting_LargeObject() + [Theory] + [InlineData(65 * 1024)] + [InlineData(2 * 1024 * 1024)] + public virtual async Task Formatting_LargeObject(int size) { // Arrange - var expectedName = "This is long so we can test large objects " + new string('a', 1024 * 65); + var expectedName = "This is long so we can test large objects " + new string('a', size); var expected = $"{{\"id\":10,\"name\":\"{expectedName}\",\"streetName\":null}}"; // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}"); + var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}/{size}"); // Assert await response.AssertStatusCodeAsync(HttpStatusCode.OK); diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs index 67ae87d4829ec1974a988dc743e3cfa66c7c1c68..b69ed7dc405e09d13037b6a9acee9f7331fe5b2e 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs @@ -44,12 +44,12 @@ namespace FormatterWebSite.Controllers ["Key3"] = null, }; - [HttpGet] - public ActionResult<SimpleModel> LargeObjectResult() => + [HttpGet("{size:int}")] + public ActionResult<SimpleModel> LargeObjectResult(int size) => new SimpleModel { Id = 10, - Name = "This is long so we can test large objects " + new string('a', 1024 * 65), + Name = "This is long so we can test large objects " + new string('a', size), }; [HttpGet]