diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs index a40ca4a856..d9f553b53d 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 354829c3b8..be60ebe994 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -5,6 +5,8 @@ using System; using System.Buffers; using System.Diagnostics; using System.IO; +using System.IO.Pipelines; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; @@ -184,9 +186,31 @@ namespace Microsoft.AspNetCore.WebUtilities // unspooled content. Copy the FileStream content first when available. if (FileStream != null) { - FileStream.Position = 0; - await FileStream.CopyToAsync(destination, 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); + + await readStream.CopyToAsync(destination, cancellationToken); + + // This is created with delete on close await FileStream.DisposeAsync(); FileStream = null; } @@ -227,10 +251,10 @@ namespace Microsoft.AspNetCore.WebUtilities FileStream = new FileStream( tempFileName, FileMode.Create, - FileAccess.ReadWrite, - FileShare.Delete, + FileAccess.Write, + FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, - FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose); + FileOptions.SequentialScan | FileOptions.DeleteOnClose); } } diff --git a/src/Http/WebUtilities/src/PagedByteBuffer.cs b/src/Http/WebUtilities/src/PagedByteBuffer.cs index cd9c19682d..5a71d51179 100644 --- a/src/Http/WebUtilities/src/PagedByteBuffer.cs +++ b/src/Http/WebUtilities/src/PagedByteBuffer.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; @@ -84,6 +85,23 @@ namespace Microsoft.AspNetCore.WebUtilities ClearBuffers(); } + public async Task MoveToAsync(PipeWriter writer, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + for (var i = 0; i < Pages.Count; i++) + { + var page = Pages[i]; + var length = (i == Pages.Count - 1) ? + _currentPageIndex : + page.Length; + + await writer.WriteAsync(page.AsMemory(0, length), cancellationToken); + } + + ClearBuffers(); + } + public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken) { ThrowIfDisposed(); diff --git a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs index 987c6388e5..007252fb49 100644 --- a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Xunit; namespace Microsoft.AspNetCore.WebUtilities @@ -383,9 +384,9 @@ namespace Microsoft.AspNetCore.WebUtilities private static byte[] ReadFileContent(FileStream fileStream) { - fileStream.Position = 0; + var fs = new FileStream(fileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite); using var memoryStream = new MemoryStream(); - fileStream.CopyTo(memoryStream); + fs.CopyTo(memoryStream); return memoryStream.ToArray(); } 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.Formatters.Xml/test/XmlSerializerOutputFormatterTest.cs b/src/Mvc/Mvc.Formatters.Xml/test/XmlSerializerOutputFormatterTest.cs index fff6d5c062..811f2eed6f 100644 --- a/src/Mvc/Mvc.Formatters.Xml/test/XmlSerializerOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Formatters.Xml/test/XmlSerializerOutputFormatterTest.cs @@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml } [Fact] - public async Task XmlSerializerOutputFormatterDoesntFlushOutputStream() + public async Task XmlSerializerOutputFormatterWritesContentLengthResponse() { // Arrange var sampleInput = new DummyClass { SampleInt = 10 }; @@ -411,10 +411,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); var response = outputFormatterContext.HttpContext.Response; - response.Body = FlushReportingStream.GetThrowingStream(); + response.Body = Stream.Null; // Act & Assert await formatter.WriteAsync(outputFormatterContext); + + Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength); } public static IEnumerable TypesForGetSupportedContentTypes diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs index b6889c1c2d..73dc60e49d 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.Body); + await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter); } } finally diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs index a04c79c638..d4bfbfa828 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs @@ -305,6 +305,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var stream = new Mock { CallBase = true }; stream.Setup(v => v.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); + stream.Setup(v => v.FlushAsync(It.IsAny())).Returns(Task.CompletedTask); stream.SetupGet(s => s.CanWrite).Returns(true); var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool.Shared, new MvcOptions()); @@ -322,6 +323,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters stream.Verify(v => v.Write(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); stream.Verify(v => v.Flush(), Times.Never()); + Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength); } private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter