From e2c267604214a00e36b5e4d7754dd6f248960502 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 20 Oct 2015 23:15:42 -0700 Subject: [PATCH] Add InputFormatter buffer pooling --- .../Formatters/InputFormatterContext.cs | 23 +- .../MvcCoreServiceCollectionExtensions.cs | 1 + .../HttpResponseStreamWriter.cs | 22 + .../Infrastructure/HttpRequestStreamReader.cs | 431 ++++++++++++++++++ .../IHttpRequestStreamReaderFactory.cs | 22 + ...emoryPoolHttpRequestStreamReaderFactory.cs | 94 ++++ .../Internal/MvcCoreMvcOptionsSetup.cs | 9 +- .../ModelBinding/BodyModelBinder.cs | 21 +- .../Properties/Resources.Designer.cs | 48 ++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 9 + .../JsonInputFormatter.cs | 132 +++--- .../Formatters/InputFormatterTest.cs | 27 +- .../HttpResponseStreamReaderTest.cs | 226 +++++++++ ...PoolHttpResponseStreamWriterFactoryTest.cs | 4 +- .../ModelBinding/BodyModelBinderTests.cs | 4 +- .../JsonInputFormatterTest.cs | 27 +- .../JsonPatchInputFormatterTest.cs | 15 +- ...ataContractSerializerInputFormatterTest.cs | 12 +- .../XmlSerializerInputFormatterTest.cs | 12 +- .../TestMvcOptions.cs | 3 +- .../TestHttpRequestStreamReaderFactory.cs | 17 + .../ViewFeatures/ViewExecutorTest.cs | 1 + 22 files changed, 1036 insertions(+), 124 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/HttpRequestStreamReader.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpRequestStreamReaderFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpRequestStreamReaderFactory.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/HttpResponseStreamReaderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/TestHttpRequestStreamReaderFactory.cs diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/InputFormatterContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/InputFormatterContext.cs index ef3d4e8b80..02964ce0b2 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/InputFormatterContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/Formatters/InputFormatterContext.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; +using System.Text; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; @@ -22,14 +24,18 @@ namespace Microsoft.AspNet.Mvc.Formatters /// /// The for recording errors. /// - /// - /// The of the model to deserialize. + /// + /// The of the model to deserialize. + /// + /// + /// A delegate which can create a for the request body. /// public InputFormatterContext( HttpContext httpContext, string modelName, ModelStateDictionary modelState, - ModelMetadata metadata) + ModelMetadata metadata, + Func readerFactory) { if (httpContext == null) { @@ -51,10 +57,16 @@ namespace Microsoft.AspNet.Mvc.Formatters throw new ArgumentNullException(nameof(metadata)); } + if (readerFactory == null) + { + throw new ArgumentNullException(nameof(readerFactory)); + } + HttpContext = httpContext; ModelName = modelName; ModelState = modelState; Metadata = metadata; + ReaderFactory = readerFactory; ModelType = metadata.ModelType; } @@ -82,5 +94,10 @@ namespace Microsoft.AspNet.Mvc.Formatters /// Gets the requested of the request body deserialization. /// public Type ModelType { get; } + + /// + /// Gets a delegate which can create a for the request body. + /// + public Func ReaderFactory { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 4b8dc5195c..299e930a92 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -138,6 +138,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton, DefaultArraySegmentPool>(); services.TryAddSingleton, DefaultArraySegmentPool>(); diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs index c56f2ec288..416d3dd2fe 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNet.Mvc /// public class HttpResponseStreamWriter : TextWriter { + private const int MinBufferSize = 128; + /// /// Default buffer size. /// @@ -42,6 +44,11 @@ namespace Microsoft.AspNet.Mvc throw new ArgumentNullException(nameof(stream)); } + if (!stream.CanWrite) + { + throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); + } + if (encoding == null) { throw new ArgumentNullException(nameof(encoding)); @@ -51,6 +58,11 @@ namespace Microsoft.AspNet.Mvc Encoding = encoding; _charBufferSize = bufferSize; + if (bufferSize < MinBufferSize) + { + bufferSize = MinBufferSize; + } + _encoder = encoding.GetEncoder(); _byteBuffer = new ArraySegment(new byte[encoding.GetMaxByteCount(bufferSize)]); _charBuffer = new ArraySegment(new char[bufferSize]); @@ -68,6 +80,11 @@ namespace Microsoft.AspNet.Mvc throw new ArgumentNullException(nameof(stream)); } + if (!stream.CanWrite) + { + throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); + } + if (encoding == null) { throw new ArgumentNullException(nameof(encoding)); @@ -83,6 +100,11 @@ namespace Microsoft.AspNet.Mvc throw new ArgumentNullException(nameof(leasedCharBuffer)); } + if (bufferSize <= 0 || bufferSize > leasedCharBuffer.Data.Count) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + var requiredLength = encoding.GetMaxByteCount(bufferSize); if (requiredLength > leasedByteBuffer.Data.Count) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/HttpRequestStreamReader.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/HttpRequestStreamReader.cs new file mode 100644 index 0000000000..2271529780 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/HttpRequestStreamReader.cs @@ -0,0 +1,431 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.Extensions.MemoryPool; + +namespace Microsoft.AspNet.Mvc.Infrastructure +{ + public class HttpRequestStreamReader : TextReader + { + private const int DefaultBufferSize = 1024; + private const int MinBufferSize = 128; + private const int MaxSharedBuilderCapacity = 360; // also the max capacity used in StringBuilderCache + + private Stream _stream; + private readonly Encoding _encoding; + private readonly Decoder _decoder; + + private readonly LeasedArraySegment _leasedByteBuffer; + private readonly LeasedArraySegment _leasedCharBuffer; + + private readonly int _byteBufferSize; + private readonly ArraySegment _byteBuffer; + private readonly ArraySegment _charBuffer; + + private int _charBufferIndex; + private int _charsRead; + private int _bytesRead; + + private bool _isBlocked; + + public HttpRequestStreamReader(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize) + { + } + + public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + _stream = stream; + _encoding = encoding; + _decoder = encoding.GetDecoder(); + + if (bufferSize < MinBufferSize) + { + bufferSize = MinBufferSize; + } + + _byteBufferSize = bufferSize; + _byteBuffer = new ArraySegment(new byte[bufferSize]); + var maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize); + _charBuffer = new ArraySegment(new char[maxCharsPerBuffer]); + } + + public HttpRequestStreamReader( + Stream stream, + Encoding encoding, + int bufferSize, + LeasedArraySegment leasedByteBuffer, + LeasedArraySegment leasedCharBuffer) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + if (leasedByteBuffer == null) + { + throw new ArgumentNullException(nameof(leasedByteBuffer)); + } + + if (leasedCharBuffer == null) + { + throw new ArgumentNullException(nameof(leasedCharBuffer)); + } + + if (bufferSize <= 0 || bufferSize > leasedByteBuffer.Data.Count) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + var requiredLength = encoding.GetMaxCharCount(bufferSize); + if (requiredLength > leasedCharBuffer.Data.Count) + { + var message = Resources.FormatHttpRequestStreamReader_InvalidBufferSize( + requiredLength, + bufferSize, + encoding.EncodingName, + typeof(Encoding).FullName, + nameof(Encoding.GetMaxCharCount)); + throw new ArgumentException(message, nameof(leasedCharBuffer)); + } + + _stream = stream; + _encoding = encoding; + _byteBufferSize = bufferSize; + _leasedByteBuffer = leasedByteBuffer; + _leasedCharBuffer = leasedCharBuffer; + + _decoder = encoding.GetDecoder(); + _byteBuffer = _leasedByteBuffer.Data; + _charBuffer = _leasedCharBuffer.Data; + } + +#if dnx451 + public override void Close() + { + Dispose(true); + } +#endif + + protected override void Dispose(bool disposing) + { + if (disposing && _stream != null) + { + _stream = null; + + if (_leasedByteBuffer != null) + { + _leasedByteBuffer.Owner.Return(_leasedByteBuffer); + } + + if (_leasedCharBuffer != null) + { + _leasedCharBuffer.Owner.Return(_leasedCharBuffer); + } + } + + base.Dispose(disposing); + } + + public override int Peek() + { + if (_stream == null) + { + throw new ObjectDisposedException("stream"); + } + + if (_charBufferIndex == _charsRead) + { + if (_isBlocked || ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer.Array[_charBuffer.Offset + _charBufferIndex]; + } + + public override int Read() + { + if (_stream == null) + { + throw new ObjectDisposedException("stream"); + } + + if (_charBufferIndex == _charsRead) + { + if (ReadIntoBuffer() == 0) + { + return -1; + } + } + + return _charBuffer.Array[_charBuffer.Offset + _charBufferIndex++]; + } + + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_stream == null) + { + throw new ObjectDisposedException("stream"); + } + + var charsRead = 0; + while (count > 0) + { + var charsRemaining = _charsRead - _charBufferIndex; + if (charsRemaining == 0) + { + charsRemaining = ReadIntoBuffer(); + } + + if (charsRemaining == 0) + { + break; // We're at EOF + } + + if (charsRemaining > count) + { + charsRemaining = count; + } + + Buffer.BlockCopy( + _charBuffer.Array, + (_charBuffer.Offset + _charBufferIndex) * 2, + buffer, + (index + charsRead) * 2, + charsRemaining * 2); + _charBufferIndex += charsRemaining; + + charsRead += charsRemaining; + count -= charsRemaining; + + // If we got back fewer chars than we asked for, then it's likely the underlying stream is blocked. + // Send the data back to the caller so they can process it. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + public override async Task ReadAsync(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_stream == null) + { + throw new ObjectDisposedException("stream"); + } + + if (_charBufferIndex == _charsRead && await ReadIntoBufferAsync() == 0) + { + return 0; + } + + var charsRead = 0; + while (count > 0) + { + // n is the characters available in _charBuffer + var n = _charsRead - _charBufferIndex; + + // charBuffer is empty, let's read from the stream + if (n == 0) + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + // We loop here so that we read in enough bytes to yield at least 1 char. + // We break out of the loop if the stream is blocked (EOF is reached). + do + { + Debug.Assert(n == 0); + _bytesRead = await _stream.ReadAsync( + _byteBuffer.Array, + _byteBuffer.Offset, + _byteBufferSize); + if (_bytesRead == 0) // EOF + { + _isBlocked = true; + break; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + Debug.Assert(n == 0); + + _charBufferIndex = 0; + n = _decoder.GetChars( + _byteBuffer.Array, + _byteBuffer.Offset, + _bytesRead, + _charBuffer.Array, + _charBuffer.Offset); + + Debug.Assert(n > 0); + + _charsRead += n; // Number of chars in StreamReader's buffer. + } + while (n == 0); + + if (n == 0) + { + break; // We're at EOF + } + } + + // Got more chars in charBuffer than the user requested + if (n > count) + { + n = count; + } + + Buffer.BlockCopy( + _charBuffer.Array, + (_charBuffer.Offset + _charBufferIndex) * 2, + buffer, + (index + charsRead) * 2, + n * 2); + + _charBufferIndex += n; + + charsRead += n; + count -= n; + + // This function shouldn't block for an indefinite amount of time, + // or reading from a network stream won't work right. If we got + // fewer bytes than we requested, then we want to break right here. + if (_isBlocked) + { + break; + } + } + + return charsRead; + } + + private int ReadIntoBuffer() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + _bytesRead = _stream.Read(_byteBuffer.Array, _byteBuffer.Offset, _byteBufferSize); + if (_bytesRead == 0) // We're at EOF + { + return _charsRead; + } + + _isBlocked = (_bytesRead < _byteBufferSize); + _charsRead += _decoder.GetChars( + _byteBuffer.Array, + _byteBuffer.Offset, + _bytesRead, + _charBuffer.Array, + _charBuffer.Offset + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + + private async Task ReadIntoBufferAsync() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; + + do + { + + _bytesRead = await _stream.ReadAsync( + _byteBuffer.Array, + _byteBuffer.Offset, + _byteBufferSize).ConfigureAwait(false); + if (_bytesRead == 0) + { + // We're at EOF + return _charsRead; + } + + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + _charsRead += _decoder.GetChars( + _byteBuffer.Array, + _byteBuffer.Offset, + _bytesRead, + _charBuffer.Array, + _charBuffer.Offset + _charsRead); + } + while (_charsRead == 0); + + return _charsRead; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpRequestStreamReaderFactory.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpRequestStreamReaderFactory.cs new file mode 100644 index 0000000000..c9f82988a8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpRequestStreamReaderFactory.cs @@ -0,0 +1,22 @@ +// 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.IO; +using System.Text; + +namespace Microsoft.AspNet.Mvc.Infrastructure +{ + /// + /// Creates instances for reading from . + /// + public interface IHttpRequestStreamReaderFactory + { + /// + /// Creates a new . + /// + /// The , usually . + /// The , usually . + /// A . + TextReader CreateReader(Stream stream, Encoding encoding); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpRequestStreamReaderFactory.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpRequestStreamReaderFactory.cs new file mode 100644 index 0000000000..287c2f3c02 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpRequestStreamReaderFactory.cs @@ -0,0 +1,94 @@ +// 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; +using System.Text; +using Microsoft.Extensions.MemoryPool; + +namespace Microsoft.AspNet.Mvc.Infrastructure +{ + /// + /// An that uses pooled buffers. + /// + public class MemoryPoolHttpRequestStreamReaderFactory : IHttpRequestStreamReaderFactory + { + /// + /// The default size of created char buffers. + /// + public static readonly int DefaultBufferSize = 1024; // 1KB - results in a 4KB byte array for UTF8. + + private readonly IArraySegmentPool _bytePool; + private readonly IArraySegmentPool _charPool; + + /// + /// Creates a new . + /// + /// + /// The for creating buffers. + /// + /// + /// The for creating buffers. + /// + public MemoryPoolHttpRequestStreamReaderFactory( + IArraySegmentPool bytePool, + IArraySegmentPool charPool) + { + if (bytePool == null) + { + throw new ArgumentNullException(nameof(bytePool)); + } + + if (charPool == null) + { + throw new ArgumentNullException(nameof(charPool)); + } + + _bytePool = bytePool; + _charPool = charPool; + } + + /// + public TextReader CreateReader(Stream stream, Encoding encoding) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + LeasedArraySegment bytes = null; + LeasedArraySegment chars = null; + + try + { + bytes = _bytePool.Lease(DefaultBufferSize); + + // We need to compute the minimum size of the char buffer based on the size of the byte buffer, + // so that we have enough room to encode the buffer in one shot. + var minimumSize = encoding.GetMaxCharCount(DefaultBufferSize); + chars = _charPool.Lease(minimumSize); + + return new HttpRequestStreamReader(stream, encoding, DefaultBufferSize, bytes, chars); + } + catch + { + if (bytes != null) + { + bytes.Owner.Return(bytes); + } + + if (chars != null) + { + chars.Owner.Return(chars); + } + + throw; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index 911312991a..e20fee051a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -5,6 +5,7 @@ using System; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Formatters; +using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.AspNet.Mvc.ModelBinding.Validation; @@ -17,12 +18,12 @@ namespace Microsoft.AspNet.Mvc.Internal /// public class MvcCoreMvcOptionsSetup : ConfigureOptions { - public MvcCoreMvcOptionsSetup() - : base(ConfigureMvc) + public MvcCoreMvcOptionsSetup(IHttpRequestStreamReaderFactory readerFactory) + : base((options) => ConfigureMvc(options, readerFactory)) { } - public static void ConfigureMvc(MvcOptions options) + public static void ConfigureMvc(MvcOptions options, IHttpRequestStreamReaderFactory readerFactory) { // Set up default error messages var messageProvider = options.ModelBindingMessageProvider; @@ -33,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.Internal // Set up ModelBinding options.ModelBinders.Add(new BinderTypeBasedModelBinder()); options.ModelBinders.Add(new ServicesModelBinder()); - options.ModelBinders.Add(new BodyModelBinder()); + options.ModelBinders.Add(new BodyModelBinder(readerFactory)); options.ModelBinders.Add(new HeaderModelBinder()); options.ModelBinders.Add(new SimpleTypeModelBinder()); options.ModelBinders.Add(new CancellationTokenModelBinder()); diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs index 739410f6bc..234e6ca1c9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Formatters; +using Microsoft.AspNet.Mvc.Infrastructure; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -15,6 +18,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public class BodyModelBinder : IModelBinder { + private readonly Func _readerFactory; + + /// + /// Creates a new . + /// + /// + /// The , used to create + /// instances for reading the request body. + /// + public BodyModelBinder(IHttpRequestStreamReaderFactory readerFactory) + { + _readerFactory = readerFactory.CreateReader; + } + /// public Task BindModelAsync(ModelBindingContext bindingContext) { @@ -59,7 +76,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding httpContext, modelBindingKey, bindingContext.ModelState, - bindingContext.ModelMetadata); + bindingContext.ModelMetadata, + _readerFactory); + var formatters = bindingContext.OperationBindingContext.InputFormatters; var formatter = formatters.FirstOrDefault(f => f.CanRead(formatterContext)); diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index e62d6a880e..597acc1cd5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1050,6 +1050,54 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("UrlNotLocal"); } + /// + /// The char buffer must have a length of at least '{0}' to be used with a byte buffer of size '{1}' and encoding '{2}'. Use '{3}.{4}' to compute the correct size for the char buffer. + /// + internal static string HttpRequestStreamReader_InvalidBufferSize + { + get { return GetString("HttpRequestStreamReader_InvalidBufferSize"); } + } + + /// + /// The char buffer must have a length of at least '{0}' to be used with a byte buffer of size '{1}' and encoding '{2}'. Use '{3}.{4}' to compute the correct size for the char buffer. + /// + internal static string FormatHttpRequestStreamReader_InvalidBufferSize(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpRequestStreamReader_InvalidBufferSize"), p0, p1, p2, p3, p4); + } + + /// + /// The stream must support reading. + /// + internal static string HttpRequestStreamReader_StreamNotReadable + { + get { return GetString("HttpRequestStreamReader_StreamNotReadable"); } + } + + /// + /// The stream must support reading. + /// + internal static string FormatHttpRequestStreamReader_StreamNotReadable() + { + return GetString("HttpRequestStreamReader_StreamNotReadable"); + } + + /// + /// The stream must support writing. + /// + internal static string HttpResponseStreamWriter_StreamNotWritable + { + get { return GetString("HttpResponseStreamWriter_StreamNotWritable"); } + } + + /// + /// The stream must support writing. + /// + internal static string FormatHttpResponseStreamWriter_StreamNotWritable() + { + return GetString("HttpResponseStreamWriter_StreamNotWritable"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index d57c5ed85d..839ec798a1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -322,4 +322,13 @@ The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local. + + The char buffer must have a length of at least '{0}' to be used with a byte buffer of size '{1}' and encoding '{2}'. Use '{3}.{4}' to compute the correct size for the char buffer. + + + The stream must support reading. + + + The stream must support writing. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonInputFormatter.cs index badf691390..7783835f0f 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonInputFormatter.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.Internal; using Microsoft.AspNet.Mvc.ModelBinding; using Newtonsoft.Json; @@ -72,98 +73,71 @@ namespace Microsoft.AspNet.Mvc.Formatters } var request = context.HttpContext.Request; - using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding)) + using (var streamReader = context.ReaderFactory(request.Body, effectiveEncoding)) { - jsonReader.CloseInput = false; - - var successful = true; - EventHandler errorHandler = (sender, eventArgs) => + using (var jsonReader = new JsonTextReader(streamReader)) { - successful = false; + jsonReader.CloseInput = false; - var exception = eventArgs.ErrorContext.Error; - - // Handle path combinations such as "" + "Property", "Parent" + "Property", or "Parent" + "[12]". - var key = eventArgs.ErrorContext.Path; - if (!string.IsNullOrEmpty(context.ModelName)) + var successful = true; + EventHandler errorHandler = (sender, eventArgs) => { - if (string.IsNullOrEmpty(eventArgs.ErrorContext.Path)) + successful = false; + + var exception = eventArgs.ErrorContext.Error; + + // Handle path combinations such as "" + "Property", "Parent" + "Property", or "Parent" + "[12]". + var key = eventArgs.ErrorContext.Path; + if (!string.IsNullOrEmpty(context.ModelName)) { - key = context.ModelName; - } - else if (eventArgs.ErrorContext.Path[0] == '[') - { - key = context.ModelName + eventArgs.ErrorContext.Path; - } - else - { - key = context.ModelName + "." + eventArgs.ErrorContext.Path; + if (string.IsNullOrEmpty(eventArgs.ErrorContext.Path)) + { + key = context.ModelName; + } + else if (eventArgs.ErrorContext.Path[0] == '[') + { + key = context.ModelName + eventArgs.ErrorContext.Path; + } + else + { + key = context.ModelName + "." + eventArgs.ErrorContext.Path; + } } + + var metadata = GetPathMetadata(context.Metadata, eventArgs.ErrorContext.Path); + context.ModelState.TryAddModelError(key, eventArgs.ErrorContext.Error, metadata); + + // Error must always be marked as handled + // Failure to do so can cause the exception to be rethrown at every recursive level and + // overflow the stack for x64 CLR processes + eventArgs.ErrorContext.Handled = true; + }; + + var type = context.ModelType; + var jsonSerializer = CreateJsonSerializer(); + jsonSerializer.Error += errorHandler; + + object model; + try + { + model = jsonSerializer.Deserialize(jsonReader, type); + } + finally + { + // Clean up the error handler in case CreateJsonSerializer() reuses a serializer + jsonSerializer.Error -= errorHandler; } - var metadata = GetPathMetadata(context.Metadata, eventArgs.ErrorContext.Path); - context.ModelState.TryAddModelError(key, eventArgs.ErrorContext.Error, metadata); + if (successful) + { + return InputFormatterResult.SuccessAsync(model); + } - // Error must always be marked as handled - // Failure to do so can cause the exception to be rethrown at every recursive level and - // overflow the stack for x64 CLR processes - eventArgs.ErrorContext.Handled = true; - }; - - var type = context.ModelType; - var jsonSerializer = CreateJsonSerializer(); - jsonSerializer.Error += errorHandler; - - object model; - try - { - model = jsonSerializer.Deserialize(jsonReader, type); + return InputFormatterResult.FailureAsync(); } - finally - { - // Clean up the error handler in case CreateJsonSerializer() reuses a serializer - jsonSerializer.Error -= errorHandler; - } - - if (successful) - { - return InputFormatterResult.SuccessAsync(model); - } - - return InputFormatterResult.FailureAsync(); } } - /// - /// Called during deserialization to get the . - /// - /// The for the read. - /// The from which to read. - /// The to use when reading. - /// The used during deserialization. - protected virtual JsonReader CreateJsonReader( - InputFormatterContext context, - Stream readStream, - Encoding effectiveEncoding) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (readStream == null) - { - throw new ArgumentNullException(nameof(readStream)); - } - - if (effectiveEncoding == null) - { - throw new ArgumentNullException(nameof(effectiveEncoding)); - } - - return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); - } - /// /// Called during deserialization to get the . /// diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs index edce8d3bfd..066aa140f5 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/InputFormatterTest.cs @@ -43,7 +43,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -76,7 +77,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -106,7 +108,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -139,7 +142,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -169,7 +173,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -205,7 +210,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -234,7 +240,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -268,7 +275,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); @@ -299,7 +307,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(context); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/HttpResponseStreamReaderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/HttpResponseStreamReaderTest.cs new file mode 100644 index 0000000000..91386c00c7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/HttpResponseStreamReaderTest.cs @@ -0,0 +1,226 @@ +// 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.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Infrastructure +{ + public class HttpResponseStreamReaderTest + { + private static readonly char[] CharData = new char[] + { + char.MinValue, + char.MaxValue, + '\t', + ' ', + '$', + '@', + '#', + '\0', + '\v', + '\'', + '\u3190', + '\uC3A0', + 'A', + '5', + '\r', + '\uFE70', + '-', + ';', + '\r', + '\n', + 'T', + '3', + '\n', + 'K', + '\u00E6', + }; + + [Fact] + public static async Task ReadToEndAsync() + { + // Arrange + var reader = new HttpRequestStreamReader(GetLargeStream(), Encoding.UTF8); + + var result = await reader.ReadToEndAsync(); + + Assert.Equal(5000, result.Length); + } + + [Fact] + public static void TestRead() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var tmp = reader.Read(); + Assert.Equal((int)CharData[i], tmp); + } + } + + [Fact] + public static void TestPeek() + { + // Arrange + var reader = CreateReader(); + + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var peek = reader.Peek(); + Assert.Equal((int)CharData[i], peek); + + reader.Read(); + } + } + + [Fact] + public static void EmptyStream() + { + // Arrange + var reader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8); + var buffer = new char[10]; + + // Act + var read = reader.Read(buffer, 0, 1); + + // Assert + Assert.Equal(0, read); + } + + [Fact] + public static void Read_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = reader.Read(chars, 0, chars.Length); + + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) + { + Assert.Equal(CharData[i], chars[i]); + } + } + + [Fact] + public static async Task Read_ReadInTwoChunks() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + + // Act + var read = await reader.ReadAsync(chars, 4, 3); + + // Assert + Assert.Equal(read, 3); + for (var i = 0; i < 3; i++) + { + Assert.Equal(CharData[i], chars[i + 4]); + } + } + + [Fact] + public static void ReadLine_ReadMultipleLines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + + // Act & Assert + var data = reader.ReadLine(); + Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); + + data = reader.ReadLine(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); + + data = reader.ReadLine(); + Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); + } + + [Fact] + public static void ReadLine_ReadWithNoNewlines() + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + var temp = new char[10]; + + // Act + reader.Read(temp, 0, 1); + var data = reader.ReadLine(); + + // Assert + Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); + } + + [Fact] + public static async Task ReadLineAsync_MultipleContinuousLines() + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("\n\n\r\r\n"); + writer.Flush(); + stream.Position = 0; + + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + + // Act & Assert + for (var i = 0; i < 4; i++) + { + var data = await reader.ReadLineAsync(); + Assert.Equal(string.Empty, data); + } + + var eol = await reader.ReadLineAsync(); + Assert.Null(eol); + } + + private static HttpRequestStreamReader CreateReader() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(CharData); + writer.Flush(); + stream.Position = 0; + + return new HttpRequestStreamReader(stream, Encoding.UTF8); + } + + private static MemoryStream GetSmallStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + return new MemoryStream(testData); + } + + private static MemoryStream GetLargeStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + // System.Collections.Generic. + + var data = new List(); + for (var i = 0; i < 1000; i++) + { + data.AddRange(testData); + } + + return new MemoryStream(data.ToArray()); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs index 26b8e3d170..e368ce6691 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure var bytePool = new Mock>(MockBehavior.Strict); bytePool .Setup(p => p.Lease(It.IsAny())) - .Returns(new LeasedArraySegment(new ArraySegment(new byte[0]), bytePool.Object)); + .Returns(new LeasedArraySegment(new ArraySegment(new byte[4096]), bytePool.Object)); bytePool .Setup(p => p.Return(It.IsAny>())) .Verifiable(); @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure var charPool = new Mock>(MockBehavior.Strict); charPool .Setup(p => p.Lease(MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize)) - .Returns(new LeasedArraySegment(new ArraySegment(new char[0]), charPool.Object)); + .Returns(new LeasedArraySegment(new ArraySegment(new char[4096]), charPool.Object)); charPool .Setup(p => p.Return(It.IsAny>())) .Verifiable(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs index 6bed04e909..5661f8eaae 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding new[] { inputFormatter }, metadataProvider: provider); - var binder = new BodyModelBinder(); + var binder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory()); // Act var binderResult = await binder.BindModelAsync(bindingContext); @@ -257,7 +257,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var operationBindingContext = new OperationBindingContext { InputFormatters = inputFormatters.ToList(), - ModelBinder = new BodyModelBinder(), + ModelBinder = new BodyModelBinder(new TestHttpRequestStreamReaderFactory()), MetadataProvider = metadataProvider, HttpContext = httpContext, }; diff --git a/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs index e11be51c8c..155446b16e 100644 --- a/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs @@ -44,7 +44,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(formatterContext); @@ -92,7 +93,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -117,7 +119,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -145,7 +148,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -173,7 +177,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -200,7 +205,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: "names", modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -228,7 +234,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); modelState.MaxAllowedErrors = 3; modelState.AddModelError("key1", "error1"); @@ -286,7 +293,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await jsonFormatter.ReadAsync(inputFormatterContext); @@ -321,7 +329,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await jsonFormatter.ReadAsync(inputFormatterContext); diff --git a/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs index 86bb77883b..e42eacf9ad 100644 --- a/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs @@ -31,7 +31,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -61,7 +62,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -96,7 +98,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(formatterContext); @@ -123,7 +126,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(formatterContext); @@ -151,7 +155,8 @@ namespace Microsoft.AspNet.Mvc.Formatters httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); diff --git a/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs index f4b3e17db3..061c58f7b4 100644 --- a/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs @@ -79,7 +79,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(formatterContext); @@ -345,7 +346,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var ex = await Assert.ThrowsAsync(expectedException, () => formatter.ReadAsync(context)); @@ -411,7 +413,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -556,7 +559,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); } private static HttpContext GetHttpContext( diff --git a/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs index 4a11d84d85..692531097c 100644 --- a/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs @@ -65,7 +65,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = formatter.CanRead(formatterContext); @@ -350,7 +351,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act and Assert var ex = await Assert.ThrowsAsync(expectedException, () => formatter.ReadAsync(context)); @@ -413,7 +415,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: modelState, - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); // Act var result = await formatter.ReadAsync(context); @@ -437,7 +440,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml httpContext, modelName: string.Empty, modelState: new ModelStateDictionary(), - metadata: metadata); + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); } private static HttpContext GetHttpContext( diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs index c0d127f4a2..c57f76e676 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs @@ -4,7 +4,6 @@ using Microsoft.AspNet.Mvc.DataAnnotations.Internal; using Microsoft.AspNet.Mvc.Formatters.Json.Internal; using Microsoft.AspNet.Mvc.Internal; -using Microsoft.AspNet.Mvc.TestCommon; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.OptionsModel; @@ -15,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests public TestMvcOptions() { Value = new MvcOptions(); - MvcCoreMvcOptionsSetup.ConfigureMvc(Value); + MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory()); var collection = new ServiceCollection().AddOptions(); MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc( Value, diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpRequestStreamReaderFactory.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpRequestStreamReaderFactory.cs new file mode 100644 index 0000000000..3bdfcc53e1 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpRequestStreamReaderFactory.cs @@ -0,0 +1,17 @@ +// 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.IO; +using System.Text; +using Microsoft.AspNet.Mvc.Infrastructure; + +namespace Microsoft.AspNet.Mvc +{ + public class TestHttpRequestStreamReaderFactory : IHttpRequestStreamReaderFactory + { + public TextReader CreateReader(Stream stream, Encoding encoding) + { + return new HttpRequestStreamReader(stream, encoding); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs index 7c9df6ac6c..81ae3de7d6 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs @@ -302,6 +302,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures var context = new DefaultHttpContext(); var stream = new Mock(); + stream.SetupGet(s => s.CanWrite).Returns(true); context.Response.Body = stream.Object; var actionContext = new ActionContext(