Enabled a way to buffer request body in formatters
This commit is contained in:
parent
8fb5652f0a
commit
af91b58bd3
|
|
@ -74,6 +74,11 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
public FormatterCollection<IInputFormatter> InputFormatters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flag to buffer the request body in input formatters. Default is <c>false</c>.
|
||||
/// </summary>
|
||||
public bool SuppressInputFormatterBuffering { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
||||
/// errors are ignored.
|
||||
|
|
|
|||
|
|
@ -67,14 +67,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal
|
|||
jsonInputPatchLogger,
|
||||
_jsonSerializerSettings,
|
||||
_charPool,
|
||||
_objectPoolProvider));
|
||||
_objectPoolProvider,
|
||||
options.SuppressInputFormatterBuffering));
|
||||
|
||||
var jsonInputLogger = _loggerFactory.CreateLogger<JsonInputFormatter>();
|
||||
options.InputFormatters.Add(new JsonInputFormatter(
|
||||
jsonInputLogger,
|
||||
_jsonSerializerSettings,
|
||||
_charPool,
|
||||
_objectPoolProvider));
|
||||
_objectPoolProvider,
|
||||
options.SuppressInputFormatterBuffering));
|
||||
|
||||
options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json"));
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -21,6 +26,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
private readonly IArrayPool<char> _charPool;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ObjectPoolProvider _objectPoolProvider;
|
||||
private readonly bool _suppressInputFormatterBuffering;
|
||||
|
||||
private ObjectPool<JsonSerializer> _jsonSerializerPool;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -38,7 +45,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
ILogger logger,
|
||||
JsonSerializerSettings serializerSettings,
|
||||
ArrayPool<char> charPool,
|
||||
ObjectPoolProvider objectPoolProvider)
|
||||
ObjectPoolProvider objectPoolProvider) :
|
||||
this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering: false)
|
||||
{
|
||||
// This constructor by default buffers the request body as its the most secure setting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="JsonInputFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="serializerSettings">
|
||||
/// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
|
||||
/// (<see cref="MvcJsonOptions.SerializerSettings"/>) or an instance
|
||||
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
|
||||
/// </param>
|
||||
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
|
||||
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
|
||||
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
|
||||
public JsonInputFormatter(
|
||||
ILogger logger,
|
||||
JsonSerializerSettings serializerSettings,
|
||||
ArrayPool<char> charPool,
|
||||
ObjectPoolProvider objectPoolProvider,
|
||||
bool suppressInputFormatterBuffering)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
|
|
@ -64,6 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
SerializerSettings = serializerSettings;
|
||||
_charPool = new JsonArrayPool<char>(charPool);
|
||||
_objectPoolProvider = objectPoolProvider;
|
||||
_suppressInputFormatterBuffering = suppressInputFormatterBuffering;
|
||||
|
||||
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||
|
|
@ -83,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
protected JsonSerializerSettings SerializerSettings { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||
InputFormatterContext context,
|
||||
Encoding encoding)
|
||||
{
|
||||
|
|
@ -98,6 +129,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
if (!request.Body.CanSeek && !_suppressInputFormatterBuffering)
|
||||
{
|
||||
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
|
||||
// read everything into a buffer, and then seek back to the beginning.
|
||||
BufferingHelper.EnableRewind(request);
|
||||
Debug.Assert(request.Body.CanSeek);
|
||||
|
||||
await request.Body.DrainAsync(CancellationToken.None);
|
||||
request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
using (var streamReader = context.ReaderFactory(request.Body, encoding))
|
||||
{
|
||||
using (var jsonReader = new JsonTextReader(streamReader))
|
||||
|
|
@ -164,15 +207,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
// or the JSON-encoded value "null". The upstream BodyModelBinder needs to
|
||||
// be notified that we don't regard this as a real input so it can register
|
||||
// a model binding error.
|
||||
return InputFormatterResult.NoValueAsync();
|
||||
return InputFormatterResult.NoValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
return InputFormatterResult.SuccessAsync(model);
|
||||
return InputFormatterResult.Success(model);
|
||||
}
|
||||
}
|
||||
|
||||
return InputFormatterResult.FailureAsync();
|
||||
return InputFormatterResult.Failure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,28 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
JsonSerializerSettings serializerSettings,
|
||||
ArrayPool<char> charPool,
|
||||
ObjectPoolProvider objectPoolProvider)
|
||||
: base(logger, serializerSettings, charPool, objectPoolProvider)
|
||||
: this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="JsonPatchInputFormatter"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="serializerSettings">
|
||||
/// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
|
||||
/// (<see cref="MvcJsonOptions.SerializerSettings"/>) or an instance
|
||||
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
|
||||
/// </param>/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
|
||||
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
|
||||
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
|
||||
public JsonPatchInputFormatter(
|
||||
ILogger logger,
|
||||
JsonSerializerSettings serializerSettings,
|
||||
ArrayPool<char> charPool,
|
||||
ObjectPoolProvider objectPoolProvider,
|
||||
bool suppressInputFormatterBuffering)
|
||||
: base(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering)
|
||||
{
|
||||
// Clear all values and only include json-patch+json value.
|
||||
SupportedMediaTypes.Clear();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal
|
|||
options.ModelMetadataDetailsProviders.Add(new DataMemberRequiredBindingMetadataProvider());
|
||||
|
||||
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
|
||||
options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
|
||||
options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter(options.SuppressInputFormatterBuffering));
|
||||
|
||||
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider("System.Xml.Linq.XObject"));
|
||||
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider("System.Xml.XmlNode"));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal
|
|||
public void Configure(MvcOptions options)
|
||||
{
|
||||
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
|
||||
options.InputFormatters.Add(new XmlSerializerInputFormatter());
|
||||
options.InputFormatters.Add(new XmlSerializerInputFormatter(options.SuppressInputFormatterBuffering));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -24,12 +28,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
private DataContractSerializerSettings _serializerSettings;
|
||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
|
||||
private readonly bool _suppressInputFormatterBuffering;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of DataContractSerializerInputFormatter
|
||||
/// </summary>
|
||||
public XmlDataContractSerializerInputFormatter()
|
||||
public XmlDataContractSerializerInputFormatter() :
|
||||
this(suppressInputFormatterBuffering: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of DataContractSerializerInputFormatter
|
||||
/// </summary>
|
||||
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
|
||||
public XmlDataContractSerializerInputFormatter(bool suppressInputFormatterBuffering)
|
||||
{
|
||||
_suppressInputFormatterBuffering = suppressInputFormatterBuffering;
|
||||
|
||||
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||
|
||||
|
|
@ -86,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
|
||||
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
|
|
@ -99,6 +115,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
if (!request.Body.CanSeek && !_suppressInputFormatterBuffering)
|
||||
{
|
||||
// XmlDataContractSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
|
||||
// read everything into a buffer, and then seek back to the beginning.
|
||||
BufferingHelper.EnableRewind(request);
|
||||
Debug.Assert(request.Body.CanSeek);
|
||||
|
||||
await request.Body.DrainAsync(CancellationToken.None);
|
||||
request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
|
||||
{
|
||||
var type = GetSerializableType(context.ModelType);
|
||||
|
|
@ -116,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
return InputFormatterResult.SuccessAsync(deserializedObject);
|
||||
return InputFormatterResult.Success(deserializedObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
|
|
@ -23,12 +27,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
{
|
||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
|
||||
private readonly bool _suppressInputFormatterBuffering;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of XmlSerializerInputFormatter.
|
||||
/// </summary>
|
||||
public XmlSerializerInputFormatter()
|
||||
: this(suppressInputFormatterBuffering: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of XmlSerializerInputFormatter.
|
||||
/// </summary>
|
||||
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
|
||||
public XmlSerializerInputFormatter(bool suppressInputFormatterBuffering)
|
||||
{
|
||||
_suppressInputFormatterBuffering = suppressInputFormatterBuffering;
|
||||
|
||||
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||
|
||||
|
|
@ -65,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||
InputFormatterContext context,
|
||||
Encoding encoding)
|
||||
{
|
||||
|
|
@ -80,6 +96,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
if (!request.Body.CanSeek && !_suppressInputFormatterBuffering)
|
||||
{
|
||||
// XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
|
||||
// read everything into a buffer, and then seek back to the beginning.
|
||||
BufferingHelper.EnableRewind(request);
|
||||
Debug.Assert(request.Body.CanSeek);
|
||||
|
||||
await request.Body.DrainAsync(CancellationToken.None);
|
||||
request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
|
||||
{
|
||||
var type = GetSerializableType(context.ModelType);
|
||||
|
|
@ -98,7 +126,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
}
|
||||
}
|
||||
|
||||
return InputFormatterResult.SuccessAsync(deserializedObject);
|
||||
return InputFormatterResult.Success(deserializedObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
|
@ -25,6 +27,92 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
|
||||
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings();
|
||||
|
||||
[Fact]
|
||||
public async Task BuffersRequestBody_ByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var content = "{name: 'Person Name', Age: '30'}";
|
||||
var logger = GetLogger();
|
||||
var formatter =
|
||||
new JsonInputFormatter(logger, _serializerSettings, ArrayPool<char>.Shared, _objectPoolProvider);
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(User));
|
||||
var context = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: string.Empty,
|
||||
modelState: modelState,
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var userModel = Assert.IsType<User>(result.Model);
|
||||
Assert.Equal("Person Name", userModel.Name);
|
||||
Assert.Equal(30, userModel.Age);
|
||||
|
||||
Assert.True(httpContext.Request.Body.CanSeek);
|
||||
httpContext.Request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
userModel = Assert.IsType<User>(result.Model);
|
||||
Assert.Equal("Person Name", userModel.Name);
|
||||
Assert.Equal(30, userModel.Age);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
|
||||
{
|
||||
// Arrange
|
||||
var content = "{name: 'Person Name', Age: '30'}";
|
||||
var logger = GetLogger();
|
||||
var formatter =
|
||||
new JsonInputFormatter(logger, _serializerSettings, ArrayPool<char>.Shared, _objectPoolProvider, suppressInputFormatterBuffering: true);
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(User));
|
||||
var context = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: string.Empty,
|
||||
modelState: modelState,
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var userModel = Assert.IsType<User>(result.Model);
|
||||
Assert.Equal("Person Name", userModel.Name);
|
||||
Assert.Equal(30, userModel.Age);
|
||||
|
||||
// Reading again should not fail as the request body should have been buffered by the formatter
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", true)]
|
||||
[InlineData("application/*", false)]
|
||||
|
|
@ -465,11 +553,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
private static HttpContext GetHttpContext(
|
||||
byte[] contentBytes,
|
||||
string contentType = "application/json")
|
||||
{
|
||||
return GetHttpContext(new MemoryStream(contentBytes), contentType);
|
||||
}
|
||||
|
||||
private static HttpContext GetHttpContext(
|
||||
Stream requestStream,
|
||||
string contentType = "application/json")
|
||||
{
|
||||
var request = new Mock<HttpRequest>();
|
||||
var headers = new Mock<IHeaderDictionary>();
|
||||
request.SetupGet(r => r.Headers).Returns(headers.Object);
|
||||
request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes));
|
||||
request.SetupGet(f => f.Body).Returns(requestStream);
|
||||
request.SetupGet(f => f.ContentType).Returns(contentType);
|
||||
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
|
|
@ -531,5 +626,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ using System.IO;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.JsonPatch;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
|
@ -23,6 +25,94 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
|
||||
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings();
|
||||
|
||||
[Fact]
|
||||
public async Task BuffersRequestBody_ByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var logger = GetLogger();
|
||||
var formatter =
|
||||
new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool<char>.Shared, _objectPoolProvider);
|
||||
var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
|
||||
var context = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: string.Empty,
|
||||
modelState: modelState,
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(result.Model);
|
||||
Assert.Equal("add", patchDoc.Operations[0].op);
|
||||
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
|
||||
Assert.Equal("John", patchDoc.Operations[0].value);
|
||||
|
||||
Assert.True(httpContext.Request.Body.CanSeek);
|
||||
httpContext.Request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(result.Model);
|
||||
Assert.Equal("add", patchDoc.Operations[0].op);
|
||||
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
|
||||
Assert.Equal("John", patchDoc.Operations[0].value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
|
||||
{
|
||||
// Arrange
|
||||
var logger = GetLogger();
|
||||
var formatter =
|
||||
new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool<char>.Shared, _objectPoolProvider, suppressInputFormatterBuffering: true);
|
||||
var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]";
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
|
||||
var context = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: string.Empty,
|
||||
modelState: modelState,
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(result.Model);
|
||||
Assert.Equal("add", patchDoc.Operations[0].op);
|
||||
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
|
||||
Assert.Equal("John", patchDoc.Operations[0].value);
|
||||
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonPatchInputFormatter_ReadsOneOperation_Successfully()
|
||||
{
|
||||
|
|
@ -209,5 +299,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -134,6 +136,84 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
Assert.True(formatter.SupportedEncodings.Any(i => i.WebName == "utf-16"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuffersRequestBody_ByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var expectedInt = 10;
|
||||
var expectedString = "TestString";
|
||||
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
|
||||
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
|
||||
|
||||
var formatter = new XmlDataContractSerializerInputFormatter();
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
var model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
|
||||
Assert.True(httpContext.Request.Body.CanSeek);
|
||||
httpContext.Request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
|
||||
{
|
||||
// Arrange
|
||||
var expectedInt = 10;
|
||||
var expectedString = "TestString";
|
||||
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
|
||||
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
|
||||
|
||||
var formatter = new XmlDataContractSerializerInputFormatter(suppressInputFormatterBuffering: true);
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/xml";
|
||||
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
var model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
|
||||
// Reading again should fail as buffering request body is disabled
|
||||
await Assert.ThrowsAsync<XmlException>(() => formatter.ReadAsync(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReadsSimpleTypes()
|
||||
{
|
||||
|
|
@ -519,6 +599,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
|
||||
{
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
return GetInputFormatterContext(httpContext, modelType);
|
||||
}
|
||||
|
||||
private InputFormatterContext GetInputFormatterContext(HttpContext httpContext, Type modelType)
|
||||
{
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(modelType);
|
||||
return new InputFormatterContext(
|
||||
|
|
@ -555,5 +640,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
return base.CreateSerializer(type);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ using System.Threading.Tasks;
|
|||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.TestCommon;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Moq;
|
||||
|
|
@ -39,6 +40,97 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
public TestLevelOne TestOne { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuffersRequestBody_ByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var expectedInt = 10;
|
||||
var expectedString = "TestString";
|
||||
var expectedDateTime = XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc);
|
||||
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
|
||||
"<sampleString>" + expectedString + "</sampleString>" +
|
||||
"<SampleDate>" + expectedDateTime + "</SampleDate></TestLevelOne>";
|
||||
|
||||
var formatter = new XmlSerializerInputFormatter();
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/json";
|
||||
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
var model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
Assert.Equal(
|
||||
XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
|
||||
model.SampleDate);
|
||||
|
||||
Assert.True(httpContext.Request.Body.CanSeek);
|
||||
httpContext.Request.Body.Seek(0L, SeekOrigin.Begin);
|
||||
|
||||
result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
Assert.Equal(
|
||||
XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
|
||||
model.SampleDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
|
||||
{
|
||||
// Arrange
|
||||
var expectedInt = 10;
|
||||
var expectedString = "TestString";
|
||||
var expectedDateTime = XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.Utc);
|
||||
|
||||
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
|
||||
"<sampleString>" + expectedString + "</sampleString>" +
|
||||
"<SampleDate>" + expectedDateTime + "</SampleDate></TestLevelOne>";
|
||||
|
||||
var formatter = new XmlSerializerInputFormatter(suppressInputFormatterBuffering: true);
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<IHttpResponseFeature>(new TestResponseFeature());
|
||||
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
|
||||
httpContext.Request.ContentType = "application/xml";
|
||||
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasError);
|
||||
var model = Assert.IsType<TestLevelOne>(result.Model);
|
||||
|
||||
Assert.Equal(expectedInt, model.SampleInt);
|
||||
Assert.Equal(expectedString, model.sampleString);
|
||||
Assert.Equal(
|
||||
XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
|
||||
model.SampleDate);
|
||||
|
||||
// Reading again should fail as buffering request body is disabled
|
||||
await Assert.ThrowsAsync<XmlException>(() => formatter.ReadAsync(context));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/xml", true)]
|
||||
[InlineData("application/*", false)]
|
||||
|
|
@ -435,6 +527,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
|
||||
{
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
return GetInputFormatterContext(httpContext, modelType);
|
||||
}
|
||||
|
||||
private InputFormatterContext GetInputFormatterContext(HttpContext httpContext, Type modelType)
|
||||
{
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(modelType);
|
||||
return new InputFormatterContext(
|
||||
|
|
@ -471,5 +568,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
return base.CreateSerializer(type);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestResponseFeature : HttpResponseFeature
|
||||
{
|
||||
public override void OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
// do not do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TestCommon
|
||||
{
|
||||
public class NonSeekableReadStream : Stream
|
||||
{
|
||||
private Stream _inner;
|
||||
|
||||
public NonSeekableReadStream(byte[] data)
|
||||
: this(new MemoryStream(data))
|
||||
{
|
||||
}
|
||||
|
||||
public NonSeekableReadStream(Stream inner)
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
|
||||
public override bool CanRead => _inner.CanRead;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
count = Math.Max(count, 1);
|
||||
return _inner.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
count = Math.Max(count, 1);
|
||||
return _inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue