Buffer writes from sources of synchronous writes (#9015)

* Buffer writes from sources of synchronous writes

Fixes https://github.com/aspnet/AspNetCore/issues/6397
This commit is contained in:
Pranav K 2019-04-17 18:11:07 -07:00 committed by GitHub
parent e4328b2a96
commit 92cae6faab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1364 additions and 188 deletions

View File

@ -306,7 +306,6 @@ namespace Microsoft.AspNetCore.Http.Internal
}
public static partial class BufferingHelper
{
public static string TempDirectory { get { throw null; } }
public static Microsoft.AspNetCore.Http.HttpRequest EnableRewind(this Microsoft.AspNetCore.Http.HttpRequest request, int bufferThreshold = 30720, long? bufferLimit = default(long?)) { throw null; }
public static Microsoft.AspNetCore.WebUtilities.MultipartSection EnableRewind(this Microsoft.AspNetCore.WebUtilities.MultipartSection section, System.Action<System.IDisposable> registerForDispose, int bufferThreshold = 30720, long? bufferLimit = default(long?)) { throw null; }
}

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.WebUtilities;
namespace Microsoft.AspNetCore.Http.Internal
@ -11,33 +11,6 @@ namespace Microsoft.AspNetCore.Http.Internal
{
internal const int DefaultBufferThreshold = 1024 * 30;
private readonly static Func<string> _getTempDirectory = () => TempDirectory;
private static string _tempDirectory;
public static string TempDirectory
{
get
{
if (_tempDirectory == null)
{
// Look for folders in the following order.
var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location.
Path.GetTempPath(); // Fall back.
if (!Directory.Exists(temp))
{
// TODO: ???
throw new DirectoryNotFoundException(temp);
}
_tempDirectory = temp;
}
return _tempDirectory;
}
}
public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
{
if (request == null)
@ -48,7 +21,7 @@ namespace Microsoft.AspNetCore.Http.Internal
var body = request.Body;
if (!body.CanSeek)
{
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
request.Body = fileStream;
request.HttpContext.Response.RegisterForDispose(fileStream);
}
@ -70,11 +43,11 @@ namespace Microsoft.AspNetCore.Http.Internal
var body = section.Body;
if (!body.CanSeek)
{
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
section.Body = fileStream;
registerForDispose(fileStream);
}
return section;
}
}
}
}

View File

@ -13,6 +13,7 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
</ItemGroup>
<ItemGroup>

View File

@ -62,6 +62,29 @@ namespace Microsoft.AspNetCore.WebUtilities
public override void Write(byte[] buffer, int offset, int count) { }
public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public sealed partial class FileBufferingWriteStream : System.IO.Stream
{
public FileBufferingWriteStream(int memoryThreshold = 32768, long? bufferLimit = default(long?), System.Func<string> tempFileDirectoryAccessor = null) { }
public override bool CanRead { get { throw null; } }
public override bool CanSeek { get { throw null; } }
public override bool CanWrite { get { throw null; } }
public override long Length { get { throw null; } }
public override long Position { get { throw null; } set { } }
protected override void Dispose(bool disposing) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.ValueTask DisposeAsync() { 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; }
public override int Read(byte[] buffer, int offset, int count) { throw null; }
public override System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; }
public override void SetLength(long value) { }
public override void Write(byte[] buffer, int offset, int count) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class FileMultipartSection
{
public FileMultipartSection(Microsoft.AspNetCore.WebUtilities.MultipartSection section) { }
@ -129,6 +152,8 @@ namespace Microsoft.AspNetCore.WebUtilities
public HttpResponseStreamWriter(System.IO.Stream stream, System.Text.Encoding encoding, int bufferSize, System.Buffers.ArrayPool<byte> bytePool, System.Buffers.ArrayPool<char> charPool) { }
public override System.Text.Encoding Encoding { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected override void Dispose(bool disposing) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
public override void Flush() { }
public override System.Threading.Tasks.Task FlushAsync() { throw null; }
public override void Write(char value) { }

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
namespace Microsoft.AspNetCore.Internal
{
internal static class AspNetCoreTempDirectory
{
private static string _tempDirectory;
public static string TempDirectory
{
get
{
if (_tempDirectory == null)
{
// Look for folders in the following order.
var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location.
Path.GetTempPath(); // Fall back.
if (!Directory.Exists(temp))
{
throw new DirectoryNotFoundException(temp);
}
_tempDirectory = temp;
}
return _tempDirectory;
}
}
public static Func<string> TempDirectoryFactory => () => TempDirectory;
}
}

View File

@ -0,0 +1,270 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.WebUtilities
{
/// <summary>
/// A <see cref="Stream"/> that buffers content to be written to disk. Use <see cref="DrainBufferAsync(Stream, CancellationToken)" />
/// to write buffered content to a target <see cref="Stream" />.
/// </summary>
public sealed class FileBufferingWriteStream : Stream
{
private const int DefaultMemoryThreshold = 32 * 1024; // 32k
private readonly int _memoryThreshold;
private readonly long? _bufferLimit;
private readonly Func<string> _tempFileDirectoryAccessor;
/// <summary>
/// Initializes a new instance of <see cref="FileBufferingWriteStream"/>.
/// </summary>
/// <param name="memoryThreshold">
/// The maximum amount of memory in bytes to allocate before switching to a file on disk.
/// Defaults to 32kb.
/// </param>
/// <param name="bufferLimit">
/// The maximum amouont of bytes that the <see cref="FileBufferingWriteStream"/> is allowed to buffer.
/// </param>
/// <param name="tempFileDirectoryAccessor">Provides the location of the directory to write buffered contents to.
/// When unspecified, uses the value specified by the environment variable <c>ASPNETCORE_TEMP</c> if available, otherwise
/// uses the value returned by <see cref="Path.GetTempPath"/>.
/// </param>
public FileBufferingWriteStream(
int memoryThreshold = DefaultMemoryThreshold,
long? bufferLimit = null,
Func<string> tempFileDirectoryAccessor = null)
{
if (memoryThreshold < 0)
{
throw new ArgumentOutOfRangeException(nameof(memoryThreshold));
}
if (bufferLimit != null && bufferLimit < memoryThreshold)
{
// We would expect a limit at least as much as memoryThreshold
throw new ArgumentOutOfRangeException(nameof(bufferLimit), $"{nameof(bufferLimit)} must be larger than {nameof(memoryThreshold)}.");
}
_memoryThreshold = memoryThreshold;
_bufferLimit = bufferLimit;
_tempFileDirectoryAccessor = tempFileDirectoryAccessor ?? AspNetCoreTempDirectory.TempDirectoryFactory;
PagedByteBuffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
}
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override bool CanWrite => true;
/// <inheritdoc />
public override long Length => throw new NotSupportedException();
/// <inheritdoc />
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
internal long BufferedLength => PagedByteBuffer.Length + (FileStream?.Length ?? 0);
internal PagedByteBuffer PagedByteBuffer { get; }
internal FileStream FileStream { get; private set; }
internal bool Disposed { get; private set; }
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
/// <inheritdoc />
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> throw new NotSupportedException();
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
ThrowArgumentException(buffer, offset, count);
ThrowIfDisposed();
if (_bufferLimit.HasValue && _bufferLimit - BufferedLength < count)
{
Dispose();
throw new IOException("Buffer limit exceeded.");
}
// Allow buffering in memory if we're below the memory threshold once the current buffer is written.
var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length;
if (allowMemoryBuffer)
{
// Buffer content in the MemoryStream if it has capacity.
PagedByteBuffer.Add(buffer, offset, count);
Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold);
}
else
{
// If the MemoryStream is incapable of accomodating the content to be written
// spool to disk.
EnsureFileStream();
// Spool memory content to disk.
PagedByteBuffer.MoveTo(FileStream);
FileStream.Write(buffer, offset, count);
}
}
/// <inheritdoc />
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ThrowArgumentException(buffer, offset, count);
ThrowIfDisposed();
if (_bufferLimit.HasValue && _bufferLimit - BufferedLength < count)
{
Dispose();
throw new IOException("Buffer limit exceeded.");
}
// Allow buffering in memory if we're below the memory threshold once the current buffer is written.
var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length;
if (allowMemoryBuffer)
{
// Buffer content in the MemoryStream if it has capacity.
PagedByteBuffer.Add(buffer, offset, count);
Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold);
}
else
{
// If the MemoryStream is incapable of accomodating the content to be written
// spool to disk.
EnsureFileStream();
// Spool memory content to disk.
await PagedByteBuffer.MoveToAsync(FileStream, cancellationToken);
await FileStream.WriteAsync(buffer, offset, count, cancellationToken);
}
}
/// <inheritdoc />
public override void Flush()
{
// Do nothing.
}
/// <inheritdoc />
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException();
/// <summary>
/// Drains buffered content to <paramref name="destination"/>.
/// </summary>
/// <param name="destination">The <see cref="Stream" /> to drain buffered contents to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken" />.</param>
/// <returns>A <see cref="Task" /> that represents the asynchronous drain operation.</returns>
public async Task DrainBufferAsync(Stream 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)
{
FileStream.Position = 0;
await FileStream.CopyToAsync(destination, cancellationToken);
await FileStream.DisposeAsync();
FileStream = null;
}
await PagedByteBuffer.MoveToAsync(destination, cancellationToken);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!Disposed)
{
Disposed = true;
PagedByteBuffer.Dispose();
FileStream?.Dispose();
}
}
/// <inheritdoc />
public override async ValueTask DisposeAsync()
{
if (!Disposed)
{
Disposed = true;
PagedByteBuffer.Dispose();
await (FileStream?.DisposeAsync() ?? default);
}
}
private void EnsureFileStream()
{
if (FileStream == null)
{
var tempFileDirectory = _tempFileDirectoryAccessor();
var tempFileName = Path.Combine(tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid() + ".tmp");
FileStream = new FileStream(
tempFileName,
FileMode.Create,
FileAccess.ReadWrite,
FileShare.Delete,
bufferSize: 1,
FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose);
}
}
private void ThrowIfDisposed()
{
if (Disposed)
{
throw new ObjectDisposedException(nameof(FileBufferingReadStream));
}
}
private static void ThrowArgumentException(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (offset < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (buffer.Length - offset < count)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
}
}
}

View File

@ -310,6 +310,25 @@ namespace Microsoft.AspNetCore.WebUtilities
base.Dispose(disposing);
}
public override async ValueTask DisposeAsync()
{
if (!_disposed)
{
_disposed = true;
try
{
await FlushInternalAsync(flushEncoder: true);
}
finally
{
_bytePool.Return(_byteBuffer);
_charPool.Return(_charBuffer);
}
}
await base.DisposeAsync();
}
// Note: our FlushInternal method does NOT flush the underlying stream. This would result in
// chunking.
private void FlushInternal(bool flushEncoder)

View File

@ -0,0 +1,134 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.WebUtilities
{
internal sealed class PagedByteBuffer : IDisposable
{
internal const int PageSize = 1024;
private readonly ArrayPool<byte> _arrayPool;
private byte[] _currentPage;
private int _currentPageIndex;
public PagedByteBuffer(ArrayPool<byte> arrayPool)
{
_arrayPool = arrayPool;
Pages = new List<byte[]>();
}
public int Length { get; private set; }
internal bool Disposed { get; private set; }
internal List<byte[]> Pages { get; }
private byte[] CurrentPage
{
get
{
if (_currentPage == null || _currentPageIndex == _currentPage.Length)
{
_currentPage = _arrayPool.Rent(PageSize);
Pages.Add(_currentPage);
_currentPageIndex = 0;
}
return _currentPage;
}
}
public void Add(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
while (count > 0)
{
var currentPage = CurrentPage;
var copyLength = Math.Min(count, currentPage.Length - _currentPageIndex);
Buffer.BlockCopy(
buffer,
offset,
currentPage,
_currentPageIndex,
copyLength);
Length += copyLength;
_currentPageIndex += copyLength;
offset += copyLength;
count -= copyLength;
}
}
public void MoveTo(Stream stream)
{
ThrowIfDisposed();
for (var i = 0; i < Pages.Count; i++)
{
var page = Pages[i];
var length = (i == Pages.Count - 1) ?
_currentPageIndex :
page.Length;
stream.Write(page, 0, length);
}
ClearBuffers();
}
public async Task MoveToAsync(Stream stream, 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 stream.WriteAsync(page, 0, length, cancellationToken);
}
ClearBuffers();
}
public void Dispose()
{
if (!Disposed)
{
Disposed = true;
ClearBuffers();
}
}
private void ClearBuffers()
{
for (var i = 0; i < Pages.Count; i++)
{
_arrayPool.Return(Pages[i]);
}
Pages.Clear();
_currentPage = null;
Length = 0;
_currentPageIndex = 0;
}
private void ThrowIfDisposed()
{
if (Disposed)
{
throw new ObjectDisposedException(nameof(PagedByteBuffer));
}
}
}
}

View File

@ -4,16 +4,16 @@
using System.IO;
using Xunit;
namespace Microsoft.AspNetCore.Http.Internal
namespace Microsoft.AspNetCore.Internal
{
public class BufferingHelperTests
public class AspNetCoreTempDirectoryTests
{
[Fact]
public void GetTempDirectory_Returns_Valid_Location()
{
var tempDirectory = BufferingHelper.TempDirectory;
var tempDirectory = AspNetCoreTempDirectory.TempDirectory;
Assert.NotNull(tempDirectory);
Assert.True(Directory.Exists(tempDirectory));
}
}
}
}

View File

@ -0,0 +1,393 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities
{
public class FileBufferingWriteStreamTests : IDisposable
{
private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), "FileBufferingWriteTests", Path.GetRandomFileName());
public FileBufferingWriteStreamTests()
{
Directory.CreateDirectory(TempDirectory);
}
[Fact]
public void Write_BuffersContentToMemory()
{
// Arrange
using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory);
var input = Encoding.UTF8.GetBytes("Hello world");
// Act
bufferingStream.Write(input, 0, input.Length);
// Assert
// We should have written content to memory
var pagedByteBuffer = bufferingStream.PagedByteBuffer;
Assert.Equal(input, ReadBufferedContent(pagedByteBuffer));
// No files should not have been created.
Assert.Null(bufferingStream.FileStream);
}
[Fact]
public void Write_BeforeMemoryThresholdIsReached_WritesToMemory()
{
// Arrange
var input = new byte[] { 1, 2, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
// Act
bufferingStream.Write(input, 0, 2);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.Null(fileStream);
// No content should be in the memory stream
Assert.Equal(2, pageBuffer.Length);
Assert.Equal(input, ReadBufferedContent(pageBuffer));
}
[Fact]
public void Write_BuffersContentToDisk_WhenMemoryThresholdIsReached()
{
// Arrange
var input = new byte[] { 1, 2, 3, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
bufferingStream.Write(input, 0, 2);
// Act
bufferingStream.Write(input, 2, 1);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(input, fileBytes);
// No content should be in the memory stream
Assert.Equal(0, pageBuffer.Length);
}
[Fact]
public void Write_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold()
{
// Arrange
var input = new byte[] { 1, 2, 3, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
// Act
bufferingStream.Write(input, 0, input.Length);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(input, fileBytes);
// No content should be in the memory stream
Assert.Equal(0, pageBuffer.Length);
}
[Fact]
public void Write_AfterMemoryThresholdIsReached_BuffersToMemory()
{
// Arrange
var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory);
// Act
bufferingStream.Write(input, 0, 5);
bufferingStream.Write(input, 5, 2);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes);
Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer));
}
[Fact]
public async Task WriteAsync_BuffersContentToMemory()
{
// Arrange
using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory);
var input = Encoding.UTF8.GetBytes("Hello world");
// Act
await bufferingStream.WriteAsync(input, 0, input.Length);
// Assert
// We should have written content to memory
var pagedByteBuffer = bufferingStream.PagedByteBuffer;
Assert.Equal(input, ReadBufferedContent(pagedByteBuffer));
// No files should not have been created.
Assert.Null(bufferingStream.FileStream);
}
[Fact]
public async Task WriteAsync_BeforeMemoryThresholdIsReached_WritesToMemory()
{
// Arrange
var input = new byte[] { 1, 2, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
// Act
await bufferingStream.WriteAsync(input, 0, 2);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.Null(fileStream);
// No content should be in the memory stream
Assert.Equal(2, pageBuffer.Length);
Assert.Equal(input, ReadBufferedContent(pageBuffer));
}
[Fact]
public async Task WriteAsync_BuffersContentToDisk_WhenMemoryThresholdIsReached()
{
// Arrange
var input = new byte[] { 1, 2, 3, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
bufferingStream.Write(input, 0, 2);
// Act
await bufferingStream.WriteAsync(input, 2, 1);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(input, fileBytes);
// No content should be in the memory stream
Assert.Equal(0, pageBuffer.Length);
}
[Fact]
public async Task WriteAsync_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold()
{
// Arrange
var input = new byte[] { 1, 2, 3, };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory);
// Act
await bufferingStream.WriteAsync(input, 0, input.Length);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(input, fileBytes);
// No content should be in the memory stream
Assert.Equal(0, pageBuffer.Length);
}
[Fact]
public async Task WriteAsync_AfterMemoryThresholdIsReached_BuffersToMemory()
{
// Arrange
var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 };
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory);
// Act
await bufferingStream.WriteAsync(input, 0, 5);
await bufferingStream.WriteAsync(input, 5, 2);
// Assert
var pageBuffer = bufferingStream.PagedByteBuffer;
var fileStream = bufferingStream.FileStream;
// File should have been created.
Assert.NotNull(fileStream);
var fileBytes = ReadFileContent(fileStream);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes);
Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer));
}
[Fact]
public void Write_Throws_IfSingleWriteExceedsBufferLimit()
{
// Arrange
var input = new byte[20];
var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
var exception = Assert.Throws<IOException>(() => bufferingStream.Write(input, 0, input.Length));
Assert.Equal("Buffer limit exceeded.", exception.Message);
Assert.True(bufferingStream.Disposed);
}
[Fact]
public void Write_Throws_IfWriteCumulativeWritesExceedsBuffersLimit()
{
// Arrange
var input = new byte[6];
var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
bufferingStream.Write(input, 0, input.Length);
var exception = Assert.Throws<IOException>(() => bufferingStream.Write(input, 0, input.Length));
Assert.Equal("Buffer limit exceeded.", exception.Message);
// Verify we return the buffer.
Assert.True(bufferingStream.Disposed);
}
[Fact]
public void Write_DoesNotThrow_IfBufferLimitIsReached()
{
// Arrange
var input = new byte[5];
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
bufferingStream.Write(input, 0, input.Length);
bufferingStream.Write(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine
// If we got here, the test succeeded.
}
[Fact]
public async Task WriteAsync_Throws_IfSingleWriteExceedsBufferLimit()
{
// Arrange
var input = new byte[20];
var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
var exception = await Assert.ThrowsAsync<IOException>(() => bufferingStream.WriteAsync(input, 0, input.Length));
Assert.Equal("Buffer limit exceeded.", exception.Message);
Assert.True(bufferingStream.Disposed);
}
[Fact]
public async Task WriteAsync_Throws_IfWriteCumulativeWritesExceedsBuffersLimit()
{
// Arrange
var input = new byte[6];
var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
await bufferingStream.WriteAsync(input, 0, input.Length);
var exception = await Assert.ThrowsAsync<IOException>(() => bufferingStream.WriteAsync(input, 0, input.Length));
Assert.Equal("Buffer limit exceeded.", exception.Message);
// Verify we return the buffer.
Assert.True(bufferingStream.Disposed);
}
[Fact]
public async Task WriteAsync_DoesNotThrow_IfBufferLimitIsReached()
{
// Arrange
var input = new byte[5];
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory);
// Act
await bufferingStream.WriteAsync(input, 0, input.Length);
await bufferingStream.WriteAsync(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine
// If we got here, the test succeeded.
}
[Fact]
public async Task DrainBufferAsync_CopiesContentFromMemoryStream()
{
// Arrange
var input = new byte[] { 1, 2, 3, 4, 5 };
using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory);
bufferingStream.Write(input, 0, input.Length);
var memoryStream = new MemoryStream();
// Act
await bufferingStream.DrainBufferAsync(memoryStream, default);
// Assert
Assert.Equal(input, memoryStream.ToArray());
}
[Fact]
public async Task DrainBufferAsync_WithContentInDisk_CopiesContentFromMemoryStream()
{
// Arrange
var input = Enumerable.Repeat((byte)0xca, 30).ToArray();
using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 21, tempFileDirectoryAccessor: () => TempDirectory);
bufferingStream.Write(input, 0, input.Length);
var memoryStream = new MemoryStream();
// Act
await bufferingStream.DrainBufferAsync(memoryStream, default);
// Assert
Assert.Equal(input, memoryStream.ToArray());
}
public void Dispose()
{
try
{
Directory.Delete(TempDirectory, recursive: true);
}
catch
{
}
}
private static byte[] ReadFileContent(FileStream fileStream)
{
fileStream.Position = 0;
using var memoryStream = new MemoryStream();
fileStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
private static byte[] ReadBufferedContent(PagedByteBuffer buffer)
{
using var memoryStream = new MemoryStream();
buffer.MoveTo(memoryStream);
return memoryStream.ToArray();
}
}
}

View File

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities.Test
namespace Microsoft.AspNetCore.WebUtilities
{
public class FormPipeReaderTests
{

View File

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities.Test
namespace Microsoft.AspNetCore.WebUtilities
{
public class HttpResponseStreamReaderTest
{

View File

@ -11,7 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities.Test
namespace Microsoft.AspNetCore.WebUtilities
{
public class HttpResponseStreamWriterTest
{

View File

@ -0,0 +1,248 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities
{
public class PagedByteBufferTest
{
[Fact]
public void Add_CreatesNewPage()
{
// Arrange
var input = Encoding.UTF8.GetBytes("Hello world");
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
// Act
buffer.Add(input, 0, input.Length);
// Assert
Assert.Single(buffer.Pages);
Assert.Equal(input.Length, buffer.Length);
Assert.Equal(input, ReadBufferedContent(buffer));
}
[Fact]
public void Add_AppendsToExistingPage()
{
// Arrange
var input1 = Encoding.UTF8.GetBytes("Hello");
var input2 = Encoding.UTF8.GetBytes("world");
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
buffer.Add(input1, 0, input1.Length);
// Act
buffer.Add(input2, 0, input2.Length);
// Assert
Assert.Single(buffer.Pages);
Assert.Equal(10, buffer.Length);
Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer));
}
[Fact]
public void Add_WithOffsets()
{
// Arrange
var input = new byte[] { 1, 2, 3, 4, 5 };
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
// Act
buffer.Add(input, 1, 3);
// Assert
Assert.Single(buffer.Pages);
Assert.Equal(3, buffer.Length);
Assert.Equal(new byte[] { 2, 3, 4 }, ReadBufferedContent(buffer));
}
[Fact]
public void Add_FillsUpBuffer()
{
// Arrange
var input1 = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize - 1).ToArray();
var input2 = new byte[] { 0xca };
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
buffer.Add(input1, 0, input1.Length);
// Act
buffer.Add(input2, 0, 1);
// Assert
Assert.Single(buffer.Pages);
Assert.Equal(PagedByteBuffer.PageSize, buffer.Length);
Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer));
}
[Fact]
public void Add_AppendsToMultiplePages()
{
// Arrange
var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize + 10).ToArray();
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
// Act
buffer.Add(input, 0, input.Length);
// Assert
Assert.Equal(2, buffer.Pages.Count);
Assert.Equal(PagedByteBuffer.PageSize + 10, buffer.Length);
Assert.Equal(input.ToArray(), ReadBufferedContent(buffer));
}
[Fact]
public void MoveTo_CopiesContentToStream()
{
// Arrange
var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray();
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
buffer.Add(input, 0, input.Length);
var stream = new MemoryStream();
// Act
buffer.MoveTo(stream);
// Assert
Assert.Equal(input, stream.ToArray());
// Verify moving new content works.
var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray();
buffer.Add(newInput, 0, newInput.Length);
stream.SetLength(0);
buffer.MoveTo(stream);
Assert.Equal(newInput, stream.ToArray());
}
[Fact]
public async Task MoveToAsync_CopiesContentToStream()
{
// Arrange
var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray();
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
buffer.Add(input, 0, input.Length);
var stream = new MemoryStream();
// Act
await buffer.MoveToAsync(stream, default);
// Assert
Assert.Equal(input, stream.ToArray());
// Verify adding and moving new content works.
var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray();
buffer.Add(newInput, 0, newInput.Length);
stream.SetLength(0);
await buffer.MoveToAsync(stream, default);
Assert.Equal(newInput, stream.ToArray());
}
[Fact]
public async Task MoveToAsync_ClearsBuffers()
{
// Arrange
var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray();
using var buffer = new PagedByteBuffer(ArrayPool<byte>.Shared);
buffer.Add(input, 0, input.Length);
var stream = new MemoryStream();
// Act
await buffer.MoveToAsync(stream, default);
// Assert
Assert.Equal(input, stream.ToArray());
// Verify copying it again works.
Assert.Equal(0, buffer.Length);
Assert.False(buffer.Disposed);
Assert.Empty(buffer.Pages);
}
[Fact]
public void MoveTo_WithClear_ReturnsBuffers()
{
// Arrange
var input = new byte[] { 1, };
var arrayPool = new Mock<ArrayPool<byte>>();
var byteArray = new byte[PagedByteBuffer.PageSize];
arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize))
.Returns(byteArray);
arrayPool.Setup(p => p.Return(byteArray, false)).Verifiable();
var memoryStream = new MemoryStream();
using (var buffer = new PagedByteBuffer(arrayPool.Object))
{
// Act
buffer.Add(input, 0, input.Length);
buffer.MoveTo(memoryStream);
// Assert
Assert.Equal(input, memoryStream.ToArray());
}
arrayPool.Verify(p => p.Rent(It.IsAny<int>()), Times.Once());
arrayPool.Verify(p => p.Return(It.IsAny<byte[]>(), It.IsAny<bool>()), Times.Once());
}
[Fact]
public async Task MoveToAsync_ReturnsBuffers()
{
// Arrange
var input = new byte[] { 1, };
var arrayPool = new Mock<ArrayPool<byte>>();
var byteArray = new byte[PagedByteBuffer.PageSize];
arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize))
.Returns(byteArray);
var memoryStream = new MemoryStream();
using (var buffer = new PagedByteBuffer(arrayPool.Object))
{
// Act
buffer.Add(input, 0, input.Length);
await buffer.MoveToAsync(memoryStream, default);
// Assert
Assert.Equal(input, memoryStream.ToArray());
}
arrayPool.Verify(p => p.Rent(It.IsAny<int>()), Times.Once());
arrayPool.Verify(p => p.Return(It.IsAny<byte[]>(), It.IsAny<bool>()), Times.Once());
}
[Fact]
public void Dispose_ReturnsBuffers_ExactlyOnce()
{
// Arrange
var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray();
var arrayPool = new Mock<ArrayPool<byte>>();
arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize))
.Returns(new byte[PagedByteBuffer.PageSize]);
var buffer = new PagedByteBuffer(arrayPool.Object);
// Act
buffer.Add(input, 0, input.Length);
buffer.Dispose();
buffer.Dispose();
arrayPool.Verify(p => p.Rent(It.IsAny<int>()), Times.Exactly(4));
arrayPool.Verify(p => p.Return(It.IsAny<byte[]>(), It.IsAny<bool>()), Times.Exactly(4));
}
private static byte[] ReadBufferedContent(PagedByteBuffer buffer)
{
using var stream = new MemoryStream();
buffer.MoveTo(stream);
return stream.ToArray();
}
}
}

View File

@ -880,6 +880,7 @@ namespace Microsoft.AspNetCore.Mvc
public int? SslPort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressAsyncSuffixInActionNames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressInputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool SuppressOutputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Mvc.ModelBinding.IValueProviderFactory> ValueProviderFactories { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
System.Collections.Generic.IEnumerator<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch>.GetEnumerator() { throw null; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }

View File

@ -105,7 +105,13 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Gets or sets the flag to buffer the request body in input formatters. Default is <c>false</c>.
/// </summary>
public bool SuppressInputFormatterBuffering { get; set; } = false;
public bool SuppressInputFormatterBuffering { get; set; }
/// <summary>
/// Gets or sets the flag that determines if buffering is disabled for output formatters that
/// synchronously write to the HTTP response body.
/// </summary>
public bool SuppressOutputFormatterBuffering { get; set; }
/// <summary>
/// Gets or sets the maximum number of validation errors that are allowed by this application before further

View File

@ -91,9 +91,7 @@ namespace Microsoft.AspNetCore.Mvc
{
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@ -10,14 +9,12 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
@ -107,9 +104,7 @@ namespace Microsoft.AspNetCore.Mvc
{
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(

View File

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
@ -93,9 +92,7 @@ namespace Microsoft.AspNetCore.Mvc
{
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(

View File

@ -2,13 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging.Abstractions;
@ -16,7 +14,6 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters
@ -468,9 +465,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
// Set up default output formatters.
MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter());
MvcOptions.OutputFormatters.Add(new StringOutputFormatter());
MvcOptions.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
MvcOptions.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
// Set up default mapping for json extensions to content type
MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
protected static ActionContext GetActionContext(
MediaTypeHeaderValue contentType,
MemoryStream responseStream = null)
Stream responseStream = null)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = contentType.ToString();
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
object outputValue,
Type outputType,
string contentType = "application/xml; charset=utf-8",
MemoryStream responseStream = null)
Stream responseStream = null)
{
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);

View File

@ -2,17 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
@ -72,9 +69,7 @@ namespace Microsoft.AspNetCore.Mvc
{
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(

View File

@ -2,18 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
@ -73,9 +70,7 @@ namespace Microsoft.AspNetCore.Mvc
{
var options = Options.Create(new MvcOptions());
options.Value.OutputFormatters.Add(new StringOutputFormatter());
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings(),
ArrayPool<char>.Shared));
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
var services = new ServiceCollection();
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(

View File

@ -9,8 +9,12 @@ using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
@ -23,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly ILogger _logger;
private DataContractSerializerSettings _serializerSettings;
private MvcOptions _mvcOptions;
/// <summary>
/// Initializes a new instance of <see cref="XmlDataContractSerializerOutputFormatter"/>
@ -254,24 +259,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var dataContractSerializer = GetCachedSerializer(wrappingType);
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
if (syncIOFeature != null)
var httpContext = context.HttpContext;
var response = httpContext.Response;
_mvcOptions ??= httpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
var responseStream = response.Body;
FileBufferingWriteStream fileBufferingWriteStream = null;
if (!_mvcOptions.SuppressOutputFormatterBuffering)
{
syncIOFeature.AllowSynchronousIO = true;
fileBufferingWriteStream = new FileBufferingWriteStream();
responseStream = fileBufferingWriteStream;
}
using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding))
try
{
using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings))
await using (var textWriter = context.WriterFactory(responseStream, writerSettings.Encoding))
{
dataContractSerializer.WriteObject(xmlWriter, value);
using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings))
{
dataContractSerializer.WriteObject(xmlWriter, value);
}
}
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
// write).
await textWriter.FlushAsync();
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
}
}
finally
{
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DisposeAsync();
}
}
}

View File

@ -9,8 +9,12 @@ using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
@ -22,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly ILogger _logger;
private MvcOptions _mvcOptions;
/// <summary>
/// Initializes a new instance of <see cref="XmlSerializerOutputFormatter"/>
@ -230,24 +235,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var xmlSerializer = GetCachedSerializer(wrappingType);
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
if (syncIOFeature != null)
var httpContext = context.HttpContext;
var response = httpContext.Response;
_mvcOptions ??= httpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
var responseStream = response.Body;
FileBufferingWriteStream fileBufferingWriteStream = null;
if (!_mvcOptions.SuppressOutputFormatterBuffering)
{
syncIOFeature.AllowSynchronousIO = true;
fileBufferingWriteStream = new FileBufferingWriteStream();
responseStream = fileBufferingWriteStream;
}
using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding))
try
{
using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings))
await using (var textWriter = context.WriterFactory(responseStream, selectedEncoding))
{
Serialize(xmlSerializer, xmlWriter, value);
using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings))
{
Serialize(xmlSerializer, xmlWriter, value);
}
}
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
// write).
await textWriter.FlushAsync();
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
}
}
finally
{
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DisposeAsync();
}
}
}
@ -281,4 +302,4 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
return (XmlSerializer)serializer;
}
}
}
}

View File

@ -10,8 +10,10 @@ using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -735,6 +737,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
request.ContentType = contentType;
httpContext.Response.Body = new MemoryStream();
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(Options.Create(new MvcOptions()))
.BuildServiceProvider();
return httpContext;
}

View File

@ -10,8 +10,10 @@ using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -520,6 +522,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
request.ContentType = contentType;
httpContext.Response.Body = new MemoryStream();
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(Options.Create(new MvcOptions()))
.BuildServiceProvider();
return httpContext;
}

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
public partial class NewtonsoftJsonOutputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter
{
public NewtonsoftJsonOutputFormatter(Newtonsoft.Json.JsonSerializerSettings serializerSettings, System.Buffers.ArrayPool<char> charPool) { }
public NewtonsoftJsonOutputFormatter(Newtonsoft.Json.JsonSerializerSettings serializerSettings, System.Buffers.ArrayPool<char> charPool, Microsoft.AspNetCore.Mvc.MvcOptions mvcOptions) { }
protected Newtonsoft.Json.JsonSerializerSettings SerializerSettings { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected virtual Newtonsoft.Json.JsonSerializer CreateJsonSerializer() { throw null; }
protected virtual Newtonsoft.Json.JsonSerializer CreateJsonSerializer(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context) { throw null; }

View File

@ -102,12 +102,6 @@ namespace Microsoft.Extensions.DependencyInjection
}
services.TryAddSingleton<IJsonHelper, NewtonsoftJsonHelper>();
services.TryAdd(ServiceDescriptor.Singleton(serviceProvider =>
{
var options = serviceProvider.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value;
var charPool = serviceProvider.GetRequiredService<ArrayPool<char>>();
return new NewtonsoftJsonOutputFormatter(options.SerializerSettings, charPool);
}));
}
}
}

View File

@ -60,7 +60,7 @@ namespace Microsoft.Extensions.DependencyInjection
public void Configure(MvcOptions options)
{
options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
options.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(_jsonOptions.SerializerSettings, _charPool));
options.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(_jsonOptions.SerializerSettings, _charPool, options));
options.InputFormatters.RemoveType<SystemTextJsonInputFormatter>();
// Register JsonPatchInputFormatter before JsonInputFormatter, otherwise

View File

@ -3,10 +3,13 @@
using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
@ -26,7 +29,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
private readonly IHttpResponseStreamWriterFactory _writerFactory;
private readonly ILogger _logger;
private readonly MvcNewtonsoftJsonOptions _options;
private readonly MvcOptions _mvcOptions;
private readonly MvcNewtonsoftJsonOptions _jsonOptions;
private readonly IArrayPool<char> _charPool;
/// <summary>
@ -34,12 +38,14 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
/// </summary>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="logger">The <see cref="ILogger{JsonResultExecutor}"/>.</param>
/// <param name="options">The <see cref="IOptions{MvcJsonOptions}"/>.</param>
/// <param name="mvcOptions">Accessor to <see cref="MvcOptions"/>.</param>
/// <param name="jsonOptions">Accessor to <see cref="MvcNewtonsoftJsonOptions"/>.</param>
/// <param name="charPool">The <see cref="ArrayPool{Char}"/> for creating <see cref="T:char[]"/> buffers.</param>
public JsonResultExecutor(
IHttpResponseStreamWriterFactory writerFactory,
ILogger<JsonResultExecutor> logger,
IOptions<MvcNewtonsoftJsonOptions> options,
IOptions<MvcOptions> mvcOptions,
IOptions<MvcNewtonsoftJsonOptions> jsonOptions,
ArrayPool<char> charPool)
{
if (writerFactory == null)
@ -52,9 +58,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
throw new ArgumentNullException(nameof(logger));
}
if (options == null)
if (jsonOptions == null)
{
throw new ArgumentNullException(nameof(options));
throw new ArgumentNullException(nameof(jsonOptions));
}
if (charPool == null)
@ -64,7 +70,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
_writerFactory = writerFactory;
_logger = logger;
_options = options.Value;
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
_jsonOptions = jsonOptions.Value;
_charPool = new JsonArrayPool<char>(charPool);
}
@ -105,10 +112,20 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
}
_logger.JsonResultExecuting(result.Value);
using (var writer = _writerFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
var responseStream = response.Body;
FileBufferingWriteStream fileBufferingWriteStream = null;
if (!_mvcOptions.SuppressOutputFormatterBuffering)
{
using (var jsonWriter = new JsonTextWriter(writer))
fileBufferingWriteStream = new FileBufferingWriteStream();
responseStream = fileBufferingWriteStream;
}
try
{
await using (var writer = _writerFactory.CreateWriter(responseStream, resolvedContentTypeEncoding))
{
using var jsonWriter = new JsonTextWriter(writer);
jsonWriter.ArrayPool = _charPool;
jsonWriter.CloseOutput = false;
jsonWriter.AutoCompleteOnClose = false;
@ -117,9 +134,17 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
jsonSerializer.Serialize(jsonWriter, result.Value);
}
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous write).
await writer.FlushAsync();
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
}
}
finally
{
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DisposeAsync();
}
}
}
@ -128,9 +153,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
var serializerSettings = result.SerializerSettings;
if (serializerSettings == null)
{
return _options.SerializerSettings;
return _jsonOptions.SerializerSettings;
}
else
else
{
if (!(serializerSettings is JsonSerializerSettings settingsFromResult))
{

View File

@ -6,7 +6,9 @@ using System.Buffers;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.Formatters
@ -17,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
public class NewtonsoftJsonOutputFormatter : TextOutputFormatter
{
private readonly IArrayPool<char> _charPool;
private readonly MvcOptions _mvcOptions;
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. We cache
// the serializer and invalidate it when the settings change.
@ -31,7 +34,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
/// </param>
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
public NewtonsoftJsonOutputFormatter(JsonSerializerSettings serializerSettings, ArrayPool<char> charPool)
/// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
public NewtonsoftJsonOutputFormatter(
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool,
MvcOptions mvcOptions)
{
if (serializerSettings == null)
{
@ -45,6 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SerializerSettings = serializerSettings;
_charPool = new JsonArrayPool<char>(charPool);
_mvcOptions = mvcOptions ?? throw new ArgumentNullException(nameof(mvcOptions));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
@ -123,26 +131,38 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
throw new ArgumentNullException(nameof(selectedEncoding));
}
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
if (syncIOFeature != null)
var response = context.HttpContext.Response;
var responseStream = response.Body;
FileBufferingWriteStream fileBufferingWriteStream = null;
if (!_mvcOptions.SuppressOutputFormatterBuffering)
{
syncIOFeature.AllowSynchronousIO = true;
fileBufferingWriteStream = new FileBufferingWriteStream();
responseStream = fileBufferingWriteStream;
}
var response = context.HttpContext.Response;
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
try
{
using (var jsonWriter = CreateJsonWriter(writer))
await using (var writer = context.WriterFactory(responseStream, selectedEncoding))
{
var jsonSerializer = CreateJsonSerializer(context);
jsonSerializer.Serialize(jsonWriter, context.Object);
using (var jsonWriter = CreateJsonWriter(writer))
{
var jsonSerializer = CreateJsonSerializer(context);
jsonSerializer.Serialize(jsonWriter, context.Object);
}
}
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
// write).
await writer.FlushAsync();
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
}
}
finally
{
if (fileBufferingWriteStream != null)
{
await fileBufferingWriteStream.DisposeAsync();
}
}
}
}

View File

@ -3,7 +3,9 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -161,10 +163,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
}
[Fact]
public async Task ExecuteAsync_ErrorDuringSerialization_DoesNotCloseTheBrackets()
public async Task ExecuteAsync_ErrorDuringSerialization_DoesNotWriteContent()
{
// Arrange
var expected = Encoding.UTF8.GetBytes("{\"name\":\"Robert\"");
var context = GetActionContext();
var result = new JsonResult(new ModelWithSerializationError());
var executor = CreateExecutor();
@ -182,7 +183,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
// Assert
var written = GetWrittenBytes(context.HttpContext);
Assert.Equal(expected, written);
Assert.Empty(written);
}
[Fact]
@ -220,63 +221,29 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
}
[Fact]
public async Task ExecuteAsync_WritesToTheResponseStream_WhenContentIsLargerThanBuffer()
public async Task ExecuteAsync_LargePayload_DoesNotPerformSynchronousWrites()
{
// Arrange
var writeLength = 2 * TestHttpResponseStreamWriterFactory.DefaultBufferSize + 4;
var text = new string('a', writeLength);
var expectedWriteCallCount = Math.Ceiling((double)writeLength / TestHttpResponseStreamWriterFactory.DefaultBufferSize);
var model = Enumerable.Range(0, 1000).Select(p => new TestModel { Property = new string('a', 5000)});
var stream = new Mock<Stream>();
var stream = new Mock<Stream> { CallBase = true };
stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
stream.SetupGet(s => s.CanWrite).Returns(true);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = stream.Object;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var context = GetActionContext();
context.HttpContext.Response.Body = stream.Object;
var result = new JsonResult(text);
var executor = CreateExecutor();
var result = new JsonResult(model);
// Act
await executor.ExecuteAsync(actionContext, result);
await executor.ExecuteAsync(context, result);
// Assert
// HttpResponseStreamWriter buffers content up to the buffer size (16k). When writes exceed the buffer size, it'll perform a synchronous
// write to the response stream.
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), TestHttpResponseStreamWriterFactory.DefaultBufferSize), Times.Exactly(2));
stream.Verify(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
// Remainder buffered content is written asynchronously as part of the FlushAsync.
stream.Verify(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once());
// Dispose does not call Flush
stream.Verify(s => s.Flush(), Times.Never());
}
[Theory]
[InlineData(5)]
[InlineData(TestHttpResponseStreamWriterFactory.DefaultBufferSize - 30)]
public async Task ExecuteAsync_DoesNotWriteSynchronouslyToTheResponseBody_WhenContentIsSmallerThanBufferSize(int writeLength)
{
// Arrange
var text = new string('a', writeLength);
var stream = new Mock<Stream>();
stream.SetupGet(s => s.CanWrite).Returns(true);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = stream.Object;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var result = new JsonResult(text);
var executor = CreateExecutor();
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
// HttpResponseStreamWriter buffers content up to the buffer size (16k) and will asynchronously write content to the response as part
// of the FlushAsync call if the content written to it is smaller than the buffer size.
// This test verifies that no synchronous writes are performed in this scenario.
stream.Verify(s => s.Flush(), Times.Never());
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
stream.Verify(v => v.Flush(), Times.Never());
}
private static JsonResultExecutor CreateExecutor(ILogger<JsonResultExecutor> logger = null)
@ -284,6 +251,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
return new JsonResultExecutor(
new TestHttpResponseStreamWriterFactory(),
logger ?? NullLogger<JsonResultExecutor>.Instance,
Options.Create(new MvcOptions()),
Options.Create(new MvcNewtonsoftJsonOptions()),
ArrayPool<char>.Shared);
}
@ -337,5 +305,10 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
MostRecentMessage = formatter(state, exception);
}
}
private class TestModel
{
public string Property { get; set; }
}
}
}

View File

@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
var executor = new JsonResultExecutor(
new TestHttpResponseStreamWriterFactory(),
NullLogger<JsonResultExecutor>.Instance,
Options.Create(new MvcOptions()),
Options.Create(new MvcNewtonsoftJsonOptions()),
ArrayPool<char>.Shared);

View File

@ -5,8 +5,11 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
protected override TextOutputFormatter GetOutputFormatter()
{
return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
}
[Fact]
@ -56,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Formatting = Formatting.Indented,
};
var expectedOutput = JsonConvert.SerializeObject(person, settings);
var jsonFormatter = new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared);
var jsonFormatter = new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared, new MvcOptions());
// Act
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
@ -274,8 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
// Arrange
var beforeMessage = "Hello World";
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
var before = new JValue(beforeMessage);
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
var memStream = new MemoryStream();
var outputFormatterContext = GetOutputFormatterContext(
beforeMessage,
@ -294,10 +296,38 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(beforeMessage, afterMessage);
}
[Fact]
public async Task WriteToStreamAsync_LargePayload_DoesNotPerformSynchronousWrites()
{
// Arrange
var model = Enumerable.Range(0, 1000).Select(p => new User { FullName = new string('a', 5000) });
var stream = new Mock<Stream> { CallBase = true };
stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
stream.SetupGet(s => s.CanWrite).Returns(true);
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
var outputFormatterContext = GetOutputFormatterContext(
model,
typeof(string),
"application/json; charset=utf-8",
stream.Object);
// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
// Assert
stream.Verify(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
stream.Verify(v => v.Flush(), Times.Never());
}
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
{
public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
: base(serializerSettings, ArrayPool<char>.Shared)
: base(serializerSettings, ArrayPool<char>.Shared, new MvcOptions())
{
}
@ -331,5 +361,5 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
public string FullName { get; set; }
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace BasicWebSite.Controllers.ContentNegotiation
public NormalController(ArrayPool<char> charPool)
{
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool);
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool, new MvcOptions());
}
public override void OnActionExecuted(ActionExecutedContext context)

View File

@ -23,7 +23,7 @@ namespace FormatterWebSite.Controllers
public JsonFormatterController(ArrayPool<char> charPool)
{
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool);
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool, new MvcOptions());
}
public IActionResult ReturnsIndentedJson()