Enabled a way to buffer request body in formatters

This commit is contained in:
Kiran Challa 2017-05-31 11:21:33 -07:00
parent 8fb5652f0a
commit af91b58bd3
13 changed files with 618 additions and 17 deletions

View File

@ -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.

View File

@ -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"));

View File

@ -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();
}
}
}

View File

@ -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();

View File

@ -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"));

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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);
}
}
}