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:
parent
e4328b2a96
commit
92cae6faab
|
|
@ -306,7 +306,6 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
}
|
}
|
||||||
public static partial class BufferingHelper
|
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.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; }
|
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; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Http.Internal
|
namespace Microsoft.AspNetCore.Http.Internal
|
||||||
|
|
@ -11,33 +11,6 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
{
|
{
|
||||||
internal const int DefaultBufferThreshold = 1024 * 30;
|
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)
|
public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
|
||||||
{
|
{
|
||||||
if (request == null)
|
if (request == null)
|
||||||
|
|
@ -48,7 +21,7 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
var body = request.Body;
|
var body = request.Body;
|
||||||
if (!body.CanSeek)
|
if (!body.CanSeek)
|
||||||
{
|
{
|
||||||
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
|
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
|
||||||
request.Body = fileStream;
|
request.Body = fileStream;
|
||||||
request.HttpContext.Response.RegisterForDispose(fileStream);
|
request.HttpContext.Response.RegisterForDispose(fileStream);
|
||||||
}
|
}
|
||||||
|
|
@ -70,11 +43,11 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
var body = section.Body;
|
var body = section.Body;
|
||||||
if (!body.CanSeek)
|
if (!body.CanSeek)
|
||||||
{
|
{
|
||||||
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
|
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory);
|
||||||
section.Body = fileStream;
|
section.Body = fileStream;
|
||||||
registerForDispose(fileStream);
|
registerForDispose(fileStream);
|
||||||
}
|
}
|
||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
|
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
||||||
|
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,29 @@ namespace Microsoft.AspNetCore.WebUtilities
|
||||||
public override void Write(byte[] buffer, int offset, int count) { }
|
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 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 partial class FileMultipartSection
|
||||||
{
|
{
|
||||||
public FileMultipartSection(Microsoft.AspNetCore.WebUtilities.MultipartSection section) { }
|
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 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; } }
|
public override System.Text.Encoding Encoding { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
protected override void Dispose(bool disposing) { }
|
protected override void Dispose(bool disposing) { }
|
||||||
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
|
public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
|
||||||
public override void Flush() { }
|
public override void Flush() { }
|
||||||
public override System.Threading.Tasks.Task FlushAsync() { throw null; }
|
public override System.Threading.Tasks.Task FlushAsync() { throw null; }
|
||||||
public override void Write(char value) { }
|
public override void Write(char value) { }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -310,6 +310,25 @@ namespace Microsoft.AspNetCore.WebUtilities
|
||||||
base.Dispose(disposing);
|
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
|
// Note: our FlushInternal method does NOT flush the underlying stream. This would result in
|
||||||
// chunking.
|
// chunking.
|
||||||
private void FlushInternal(bool flushEncoder)
|
private void FlushInternal(bool flushEncoder)
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Http.Internal
|
namespace Microsoft.AspNetCore.Internal
|
||||||
{
|
{
|
||||||
public class BufferingHelperTests
|
public class AspNetCoreTempDirectoryTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetTempDirectory_Returns_Valid_Location()
|
public void GetTempDirectory_Returns_Valid_Location()
|
||||||
{
|
{
|
||||||
var tempDirectory = BufferingHelper.TempDirectory;
|
var tempDirectory = AspNetCoreTempDirectory.TempDirectory;
|
||||||
Assert.NotNull(tempDirectory);
|
Assert.NotNull(tempDirectory);
|
||||||
Assert.True(Directory.Exists(tempDirectory));
|
Assert.True(Directory.Exists(tempDirectory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.WebUtilities.Test
|
namespace Microsoft.AspNetCore.WebUtilities
|
||||||
{
|
{
|
||||||
public class FormPipeReaderTests
|
public class FormPipeReaderTests
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.WebUtilities.Test
|
namespace Microsoft.AspNetCore.WebUtilities
|
||||||
{
|
{
|
||||||
public class HttpResponseStreamReaderTest
|
public class HttpResponseStreamReaderTest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.WebUtilities.Test
|
namespace Microsoft.AspNetCore.WebUtilities
|
||||||
{
|
{
|
||||||
public class HttpResponseStreamWriterTest
|
public class HttpResponseStreamWriterTest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -880,6 +880,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
public int? SslPort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
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 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 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; } }
|
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.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; }
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,13 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the flag to buffer the request body in input formatters. Default is <c>false</c>.
|
/// Gets or sets the flag to buffer the request body in input formatters. Default is <c>false</c>.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
var options = Options.Create(new MvcOptions());
|
var options = Options.Create(new MvcOptions());
|
||||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -10,14 +9,12 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -107,9 +104,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
var options = Options.Create(new MvcOptions());
|
var options = Options.Create(new MvcOptions());
|
||||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
@ -93,9 +92,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
var options = Options.Create(new MvcOptions());
|
var options = Options.Create(new MvcOptions());
|
||||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
@ -16,7 +14,6 @@ using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
@ -468,9 +465,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
// Set up default output formatters.
|
// Set up default output formatters.
|
||||||
MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter());
|
MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter());
|
||||||
MvcOptions.OutputFormatters.Add(new StringOutputFormatter());
|
MvcOptions.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
MvcOptions.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
MvcOptions.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
// Set up default mapping for json extensions to content type
|
// Set up default mapping for json extensions to content type
|
||||||
MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
|
MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat(
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
||||||
protected static ActionContext GetActionContext(
|
protected static ActionContext GetActionContext(
|
||||||
MediaTypeHeaderValue contentType,
|
MediaTypeHeaderValue contentType,
|
||||||
MemoryStream responseStream = null)
|
Stream responseStream = null)
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.ContentType = contentType.ToString();
|
httpContext.Request.ContentType = contentType.ToString();
|
||||||
|
|
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
object outputValue,
|
object outputValue,
|
||||||
Type outputType,
|
Type outputType,
|
||||||
string contentType = "application/xml; charset=utf-8",
|
string contentType = "application/xml; charset=utf-8",
|
||||||
MemoryStream responseStream = null)
|
Stream responseStream = null)
|
||||||
{
|
{
|
||||||
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,14 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -72,9 +69,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
var options = Options.Create(new MvcOptions());
|
var options = Options.Create(new MvcOptions());
|
||||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,15 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -73,9 +70,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
var options = Options.Create(new MvcOptions());
|
var options = Options.Create(new MvcOptions());
|
||||||
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
options.Value.OutputFormatters.Add(new StringOutputFormatter());
|
||||||
options.Value.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(
|
options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new MvcOptions()));
|
||||||
new JsonSerializerSettings(),
|
|
||||||
ArrayPool<char>.Shared));
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
services.AddSingleton<IActionResultExecutor<ObjectResult>>(new ObjectResultExecutor(
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ using System.Runtime.Serialization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
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 ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private DataContractSerializerSettings _serializerSettings;
|
private DataContractSerializerSettings _serializerSettings;
|
||||||
|
private MvcOptions _mvcOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="XmlDataContractSerializerOutputFormatter"/>
|
/// Initializes a new instance of <see cref="XmlDataContractSerializerOutputFormatter"/>
|
||||||
|
|
@ -254,24 +259,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
||||||
var dataContractSerializer = GetCachedSerializer(wrappingType);
|
var dataContractSerializer = GetCachedSerializer(wrappingType);
|
||||||
|
|
||||||
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
|
var httpContext = context.HttpContext;
|
||||||
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
|
var response = httpContext.Response;
|
||||||
if (syncIOFeature != null)
|
|
||||||
|
_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
|
if (fileBufferingWriteStream != null)
|
||||||
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
|
{
|
||||||
// write).
|
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
|
||||||
await textWriter.FlushAsync();
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (fileBufferingWriteStream != null)
|
||||||
|
{
|
||||||
|
await fileBufferingWriteStream.DisposeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
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 ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private MvcOptions _mvcOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="XmlSerializerOutputFormatter"/>
|
/// Initializes a new instance of <see cref="XmlSerializerOutputFormatter"/>
|
||||||
|
|
@ -230,24 +235,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
||||||
var xmlSerializer = GetCachedSerializer(wrappingType);
|
var xmlSerializer = GetCachedSerializer(wrappingType);
|
||||||
|
|
||||||
// Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397
|
var httpContext = context.HttpContext;
|
||||||
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
|
var response = httpContext.Response;
|
||||||
if (syncIOFeature != null)
|
|
||||||
|
_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
|
if (fileBufferingWriteStream != null)
|
||||||
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
|
{
|
||||||
// write).
|
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
|
||||||
await textWriter.FlushAsync();
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (fileBufferingWriteStream != null)
|
||||||
|
{
|
||||||
|
await fileBufferingWriteStream.DisposeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,4 +302,4 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
return (XmlSerializer)serializer;
|
return (XmlSerializer)serializer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -735,6 +737,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||||
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
|
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
|
||||||
request.ContentType = contentType;
|
request.ContentType = contentType;
|
||||||
httpContext.Response.Body = new MemoryStream();
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
httpContext.RequestServices = new ServiceCollection()
|
||||||
|
.AddSingleton(Options.Create(new MvcOptions()))
|
||||||
|
.BuildServiceProvider();
|
||||||
return httpContext;
|
return httpContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -520,6 +522,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||||
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
|
request.Headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset.ToString();
|
||||||
request.ContentType = contentType;
|
request.ContentType = contentType;
|
||||||
httpContext.Response.Body = new MemoryStream();
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
httpContext.RequestServices = new ServiceCollection()
|
||||||
|
.AddSingleton(Options.Create(new MvcOptions()))
|
||||||
|
.BuildServiceProvider();
|
||||||
return httpContext;
|
return httpContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
public partial class NewtonsoftJsonOutputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter
|
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 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() { throw null; }
|
||||||
protected virtual Newtonsoft.Json.JsonSerializer CreateJsonSerializer(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context) { throw null; }
|
protected virtual Newtonsoft.Json.JsonSerializer CreateJsonSerializer(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context) { throw null; }
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
}
|
}
|
||||||
|
|
||||||
services.TryAddSingleton<IJsonHelper, NewtonsoftJsonHelper>();
|
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);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
public void Configure(MvcOptions options)
|
public void Configure(MvcOptions options)
|
||||||
{
|
{
|
||||||
options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
|
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>();
|
options.InputFormatters.RemoveType<SystemTextJsonInputFormatter>();
|
||||||
// Register JsonPatchInputFormatter before JsonInputFormatter, otherwise
|
// Register JsonPatchInputFormatter before JsonInputFormatter, otherwise
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
@ -26,7 +29,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
|
|
||||||
private readonly IHttpResponseStreamWriterFactory _writerFactory;
|
private readonly IHttpResponseStreamWriterFactory _writerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly MvcNewtonsoftJsonOptions _options;
|
private readonly MvcOptions _mvcOptions;
|
||||||
|
private readonly MvcNewtonsoftJsonOptions _jsonOptions;
|
||||||
private readonly IArrayPool<char> _charPool;
|
private readonly IArrayPool<char> _charPool;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -34,12 +38,14 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
|
||||||
/// <param name="logger">The <see cref="ILogger{JsonResultExecutor}"/>.</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>
|
/// <param name="charPool">The <see cref="ArrayPool{Char}"/> for creating <see cref="T:char[]"/> buffers.</param>
|
||||||
public JsonResultExecutor(
|
public JsonResultExecutor(
|
||||||
IHttpResponseStreamWriterFactory writerFactory,
|
IHttpResponseStreamWriterFactory writerFactory,
|
||||||
ILogger<JsonResultExecutor> logger,
|
ILogger<JsonResultExecutor> logger,
|
||||||
IOptions<MvcNewtonsoftJsonOptions> options,
|
IOptions<MvcOptions> mvcOptions,
|
||||||
|
IOptions<MvcNewtonsoftJsonOptions> jsonOptions,
|
||||||
ArrayPool<char> charPool)
|
ArrayPool<char> charPool)
|
||||||
{
|
{
|
||||||
if (writerFactory == null)
|
if (writerFactory == null)
|
||||||
|
|
@ -52,9 +58,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
throw new ArgumentNullException(nameof(logger));
|
throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options == null)
|
if (jsonOptions == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(jsonOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charPool == null)
|
if (charPool == null)
|
||||||
|
|
@ -64,7 +70,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
|
|
||||||
_writerFactory = writerFactory;
|
_writerFactory = writerFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_options = options.Value;
|
_mvcOptions = mvcOptions?.Value ?? throw new ArgumentNullException(nameof(mvcOptions));
|
||||||
|
_jsonOptions = jsonOptions.Value;
|
||||||
_charPool = new JsonArrayPool<char>(charPool);
|
_charPool = new JsonArrayPool<char>(charPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,10 +112,20 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.JsonResultExecuting(result.Value);
|
_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.ArrayPool = _charPool;
|
||||||
jsonWriter.CloseOutput = false;
|
jsonWriter.CloseOutput = false;
|
||||||
jsonWriter.AutoCompleteOnClose = false;
|
jsonWriter.AutoCompleteOnClose = false;
|
||||||
|
|
@ -117,9 +134,17 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
jsonSerializer.Serialize(jsonWriter, result.Value);
|
jsonSerializer.Serialize(jsonWriter, result.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
|
if (fileBufferingWriteStream != null)
|
||||||
// buffers. This is better than just letting dispose handle it (which would result in a synchronous write).
|
{
|
||||||
await writer.FlushAsync();
|
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;
|
var serializerSettings = result.SerializerSettings;
|
||||||
if (serializerSettings == null)
|
if (serializerSettings == null)
|
||||||
{
|
{
|
||||||
return _options.SerializerSettings;
|
return _jsonOptions.SerializerSettings;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!(serializerSettings is JsonSerializerSettings settingsFromResult))
|
if (!(serializerSettings is JsonSerializerSettings settingsFromResult))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ using System.Buffers;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
@ -17,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
public class NewtonsoftJsonOutputFormatter : TextOutputFormatter
|
public class NewtonsoftJsonOutputFormatter : TextOutputFormatter
|
||||||
{
|
{
|
||||||
private readonly IArrayPool<char> _charPool;
|
private readonly IArrayPool<char> _charPool;
|
||||||
|
private readonly MvcOptions _mvcOptions;
|
||||||
|
|
||||||
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. We cache
|
// Perf: JsonSerializers are relatively expensive to create, and are thread safe. We cache
|
||||||
// the serializer and invalidate it when the settings change.
|
// the serializer and invalidate it when the settings change.
|
||||||
|
|
@ -31,7 +34,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
|
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</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)
|
if (serializerSettings == null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,6 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
||||||
SerializerSettings = serializerSettings;
|
SerializerSettings = serializerSettings;
|
||||||
_charPool = new JsonArrayPool<char>(charPool);
|
_charPool = new JsonArrayPool<char>(charPool);
|
||||||
|
_mvcOptions = mvcOptions ?? throw new ArgumentNullException(nameof(mvcOptions));
|
||||||
|
|
||||||
SupportedEncodings.Add(Encoding.UTF8);
|
SupportedEncodings.Add(Encoding.UTF8);
|
||||||
SupportedEncodings.Add(Encoding.Unicode);
|
SupportedEncodings.Add(Encoding.Unicode);
|
||||||
|
|
@ -123,26 +131,38 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
throw new ArgumentNullException(nameof(selectedEncoding));
|
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 response = context.HttpContext.Response;
|
||||||
var syncIOFeature = context.HttpContext.Features.Get<Http.Features.IHttpBodyControlFeature>();
|
|
||||||
if (syncIOFeature != null)
|
var responseStream = response.Body;
|
||||||
|
FileBufferingWriteStream fileBufferingWriteStream = null;
|
||||||
|
if (!_mvcOptions.SuppressOutputFormatterBuffering)
|
||||||
{
|
{
|
||||||
syncIOFeature.AllowSynchronousIO = true;
|
fileBufferingWriteStream = new FileBufferingWriteStream();
|
||||||
|
responseStream = fileBufferingWriteStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = context.HttpContext.Response;
|
try
|
||||||
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
|
|
||||||
{
|
{
|
||||||
using (var jsonWriter = CreateJsonWriter(writer))
|
await using (var writer = context.WriterFactory(responseStream, selectedEncoding))
|
||||||
{
|
{
|
||||||
var jsonSerializer = CreateJsonSerializer(context);
|
using (var jsonWriter = CreateJsonWriter(writer))
|
||||||
jsonSerializer.Serialize(jsonWriter, context.Object);
|
{
|
||||||
|
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
|
if (fileBufferingWriteStream != null)
|
||||||
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
|
{
|
||||||
// write).
|
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
|
||||||
await writer.FlushAsync();
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (fileBufferingWriteStream != null)
|
||||||
|
{
|
||||||
|
await fileBufferingWriteStream.DisposeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -161,10 +163,9 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExecuteAsync_ErrorDuringSerialization_DoesNotCloseTheBrackets()
|
public async Task ExecuteAsync_ErrorDuringSerialization_DoesNotWriteContent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = Encoding.UTF8.GetBytes("{\"name\":\"Robert\"");
|
|
||||||
var context = GetActionContext();
|
var context = GetActionContext();
|
||||||
var result = new JsonResult(new ModelWithSerializationError());
|
var result = new JsonResult(new ModelWithSerializationError());
|
||||||
var executor = CreateExecutor();
|
var executor = CreateExecutor();
|
||||||
|
|
@ -182,7 +183,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var written = GetWrittenBytes(context.HttpContext);
|
var written = GetWrittenBytes(context.HttpContext);
|
||||||
Assert.Equal(expected, written);
|
Assert.Empty(written);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -220,63 +221,29 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExecuteAsync_WritesToTheResponseStream_WhenContentIsLargerThanBuffer()
|
public async Task ExecuteAsync_LargePayload_DoesNotPerformSynchronousWrites()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var writeLength = 2 * TestHttpResponseStreamWriterFactory.DefaultBufferSize + 4;
|
var model = Enumerable.Range(0, 1000).Select(p => new TestModel { Property = new string('a', 5000)});
|
||||||
var text = new string('a', writeLength);
|
|
||||||
var expectedWriteCallCount = Math.Ceiling((double)writeLength / TestHttpResponseStreamWriterFactory.DefaultBufferSize);
|
|
||||||
|
|
||||||
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);
|
stream.SetupGet(s => s.CanWrite).Returns(true);
|
||||||
var httpContext = new DefaultHttpContext();
|
var context = GetActionContext();
|
||||||
httpContext.Response.Body = stream.Object;
|
context.HttpContext.Response.Body = stream.Object;
|
||||||
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
|
||||||
|
|
||||||
var result = new JsonResult(text);
|
|
||||||
var executor = CreateExecutor();
|
var executor = CreateExecutor();
|
||||||
|
var result = new JsonResult(model);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await executor.ExecuteAsync(actionContext, result);
|
await executor.ExecuteAsync(context, result);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// HttpResponseStreamWriter buffers content up to the buffer size (16k). When writes exceed the buffer size, it'll perform a synchronous
|
stream.Verify(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
|
||||||
// write to the response stream.
|
|
||||||
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), TestHttpResponseStreamWriterFactory.DefaultBufferSize), Times.Exactly(2));
|
|
||||||
|
|
||||||
// Remainder buffered content is written asynchronously as part of the FlushAsync.
|
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
|
||||||
stream.Verify(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once());
|
stream.Verify(v => v.Flush(), Times.Never());
|
||||||
|
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonResultExecutor CreateExecutor(ILogger<JsonResultExecutor> logger = null)
|
private static JsonResultExecutor CreateExecutor(ILogger<JsonResultExecutor> logger = null)
|
||||||
|
|
@ -284,6 +251,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
return new JsonResultExecutor(
|
return new JsonResultExecutor(
|
||||||
new TestHttpResponseStreamWriterFactory(),
|
new TestHttpResponseStreamWriterFactory(),
|
||||||
logger ?? NullLogger<JsonResultExecutor>.Instance,
|
logger ?? NullLogger<JsonResultExecutor>.Instance,
|
||||||
|
Options.Create(new MvcOptions()),
|
||||||
Options.Create(new MvcNewtonsoftJsonOptions()),
|
Options.Create(new MvcNewtonsoftJsonOptions()),
|
||||||
ArrayPool<char>.Shared);
|
ArrayPool<char>.Shared);
|
||||||
}
|
}
|
||||||
|
|
@ -337,5 +305,10 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
MostRecentMessage = formatter(state, exception);
|
MostRecentMessage = formatter(state, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestModel
|
||||||
|
{
|
||||||
|
public string Property { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
var executor = new JsonResultExecutor(
|
var executor = new JsonResultExecutor(
|
||||||
new TestHttpResponseStreamWriterFactory(),
|
new TestHttpResponseStreamWriterFactory(),
|
||||||
NullLogger<JsonResultExecutor>.Instance,
|
NullLogger<JsonResultExecutor>.Instance,
|
||||||
|
Options.Create(new MvcOptions()),
|
||||||
Options.Create(new MvcNewtonsoftJsonOptions()),
|
Options.Create(new MvcNewtonsoftJsonOptions()),
|
||||||
ArrayPool<char>.Shared);
|
ArrayPool<char>.Shared);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
protected override TextOutputFormatter GetOutputFormatter()
|
protected override TextOutputFormatter GetOutputFormatter()
|
||||||
{
|
{
|
||||||
return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -56,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Formatting = Formatting.Indented,
|
Formatting = Formatting.Indented,
|
||||||
};
|
};
|
||||||
var expectedOutput = JsonConvert.SerializeObject(person, settings);
|
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
|
// Act
|
||||||
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
||||||
|
|
@ -274,8 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var beforeMessage = "Hello World";
|
var beforeMessage = "Hello World";
|
||||||
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
|
||||||
var before = new JValue(beforeMessage);
|
|
||||||
var memStream = new MemoryStream();
|
var memStream = new MemoryStream();
|
||||||
var outputFormatterContext = GetOutputFormatterContext(
|
var outputFormatterContext = GetOutputFormatterContext(
|
||||||
beforeMessage,
|
beforeMessage,
|
||||||
|
|
@ -294,10 +296,38 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Equal(beforeMessage, afterMessage);
|
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
|
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
|
||||||
{
|
{
|
||||||
public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
|
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; }
|
public string FullName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ namespace BasicWebSite.Controllers.ContentNegotiation
|
||||||
|
|
||||||
public NormalController(ArrayPool<char> charPool)
|
public NormalController(ArrayPool<char> charPool)
|
||||||
{
|
{
|
||||||
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool);
|
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool, new MvcOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnActionExecuted(ActionExecutedContext context)
|
public override void OnActionExecuted(ActionExecutedContext context)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace FormatterWebSite.Controllers
|
||||||
|
|
||||||
public JsonFormatterController(ArrayPool<char> charPool)
|
public JsonFormatterController(ArrayPool<char> charPool)
|
||||||
{
|
{
|
||||||
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool);
|
_indentingFormatter = new NewtonsoftJsonOutputFormatter(_indentedSettings, charPool, new MvcOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult ReturnsIndentedJson()
|
public IActionResult ReturnsIndentedJson()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue