diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index 85a91f7d67..30de18e604 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs @@ -77,6 +77,8 @@ 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 bea934bb43..2c96ed29ae 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; @@ -185,8 +186,25 @@ namespace Microsoft.AspNetCore.WebUtilities // unspooled content. Copy the FileStream content first when available. if (FileStream != null) { - await FileStream.FlushAsync(cancellationToken); + // 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 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 3d39adde9a..3e19a8c31b 100644 --- a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs @@ -371,24 +371,6 @@ 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 e6243fcb45..c0d929ecea 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.Body); + await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter); } } finally diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs index e98d4477bd..d184f54b21 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs @@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters if (fileBufferingWriteStream != null) { response.ContentLength = fileBufferingWriteStream.Length; - await fileBufferingWriteStream.DrainBufferAsync(response.Body); + await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter); } } finally diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs index 40ea303614..c5c4ae51fd 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -154,17 +154,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(expected, await response.Content.ReadAsStringAsync()); } - [Theory] - [InlineData(65 * 1024)] - [InlineData(2 * 1024 * 1024)] - public virtual async Task Formatting_LargeObject(int size) + [Fact] + public virtual async Task Formatting_LargeObject() { // Arrange - var expectedName = "This is long so we can test large objects " + new string('a', size); + var expectedName = "This is long so we can test large objects " + new string('a', 1024 * 65); var expected = $"{{\"id\":10,\"name\":\"{expectedName}\",\"streetName\":null}}"; // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}/{size}"); + var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}"); // 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 b69ed7dc40..67ae87d482 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("{size:int}")] - public ActionResult LargeObjectResult(int size) => + [HttpGet] + public ActionResult LargeObjectResult() => new SimpleModel { Id = 10, - Name = "This is long so we can test large objects " + new string('a', size), + Name = "This is long so we can test large objects " + new string('a', 1024 * 65), }; [HttpGet]