[Fixes 3961] Consider InputFormatter behavior when we can't read the charset

* Broken InputFormatter into InputFormatter and TextInputFormatter
* Added an exception to ModelState inside ReadAsync on TextInputFormatter
  when we can't find a valid encoding to read the body.
This commit is contained in:
jacalvar 2016-02-01 15:58:18 -08:00
parent 8e8de413f8
commit fe639f028f
13 changed files with 420 additions and 133 deletions

View File

@ -3,14 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
{ {
@ -19,33 +14,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </summary> /// </summary>
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
{ {
/// <summary>
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
/// </summary>
protected static readonly Encoding UTF8EncodingWithoutBOM
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
/// <summary>
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
/// </summary>
protected static readonly Encoding UTF16EncodingLittleEndian
= new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true);
/// <summary>
/// Gets the mutable collection of character encodings supported by
/// this <see cref="InputFormatter"/>. The encodings are
/// used when reading the data.
/// </summary>
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
/// <summary> /// <summary>
/// Gets the mutable collection of media type elements supported by /// Gets the mutable collection of media type elements supported by
/// this <see cref="InputFormatter"/>. /// this <see cref="InputFormatter"/>.
/// </summary> /// </summary>
public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection(); public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection();
protected object GetDefaultValueForType(Type modelType) /// <summary>
/// Gets the default value for a given type. Used to return a default value when the body contains no content.
/// </summary>
/// <param name="modelType">The type of the value.</param>
/// <returns>The default value for the <paramref name="modelType"/> type.</returns>
protected virtual object GetDefaultValueForType(Type modelType)
{ {
if (modelType == null)
{
throw new ArgumentNullException(nameof(modelType));
}
if (modelType.GetTypeInfo().IsValueType) if (modelType.GetTypeInfo().IsValueType)
{ {
return Activator.CreateInstance(modelType); return Activator.CreateInstance(modelType);
@ -101,6 +87,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context) public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{ {
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
if (request.ContentLength == 0) if (request.ContentLength == 0)
{ {
@ -117,50 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns> /// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context); public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
/// <summary>
/// Returns an <see cref="Encoding"/> based on <paramref name="context"/>'s
/// character set.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>
/// An <see cref="Encoding"/> based on <paramref name="context"/>'s
/// character set. <c>null</c> if no supported encoding was found.
/// </returns>
protected Encoding SelectCharacterEncoding(InputFormatterContext context)
{
var request = context.HttpContext.Request;
if (request.ContentType != null)
{
var encoding = MediaType.GetEncoding(request.ContentType);
if (encoding != null)
{
foreach (var supportedEncoding in SupportedEncodings)
{
if (string.Equals(
encoding.WebName,
supportedEncoding.WebName,
StringComparison.OrdinalIgnoreCase))
{
return supportedEncoding;
}
}
}
}
if (SupportedEncodings.Count > 0)
{
return SupportedEncodings[0];
}
// No supported encoding was found so there is no way for us to start reading.
context.ModelState.TryAddModelError(
context.ModelName,
Resources.FormatInputFormatterNoEncoding(GetType().FullName));
return null;
}
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType) public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
{ {

View File

@ -0,0 +1,124 @@
// 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
/// <summary>
/// Reads an object from a request body with a text format.
/// </summary>
public abstract class TextInputFormatter : InputFormatter
{
/// <summary>
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
/// </summary>
protected static readonly Encoding UTF8EncodingWithoutBOM
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
/// <summary>
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
/// </summary>
protected static readonly Encoding UTF16EncodingLittleEndian
= new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true);
/// <summary>
/// Gets the mutable collection of character encodings supported by
/// this <see cref="TextInputFormatter"/>. The encodings are
/// used when reading the data.
/// </summary>
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
/// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.HttpContext.Request;
var selectedEncoding = SelectCharacterEncoding(context);
if (selectedEncoding == null)
{
var message = Resources.FormatUnsupportedContentType(
context.HttpContext.Request.ContentType);
var exception = new UnsupportedContentTypeException(message);
context.ModelState.AddModelError(context.ModelName, exception, context.Metadata);
return InputFormatterResult.FailureAsync();
}
return ReadRequestBodyAsync(context, selectedEncoding);
}
/// <summary>
/// Reads an object from the request body.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <param name="encoding">The <see cref="Encoding"/> used to read the request body.</param>
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding);
/// <summary>
/// Returns an <see cref="Encoding"/> based on <paramref name="context"/>'s
/// character set.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>
/// An <see cref="Encoding"/> based on <paramref name="context"/>'s
/// character set. <c>null</c> if no supported encoding was found.
/// </returns>
protected Encoding SelectCharacterEncoding(InputFormatterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (SupportedEncodings?.Count == 0)
{
var message = Resources.FormatTextInputFormatter_SupportedEncodingsMustNotBeEmpty(nameof(SupportedEncodings));
throw new InvalidOperationException(message);
}
var request = context.HttpContext.Request;
var requestEncoding = request.ContentType == null ? null : MediaType.GetEncoding(request.ContentType);
if (requestEncoding != null)
{
for (int i = 0; i < SupportedEncodings.Count; i++)
{
if (string.Equals(
requestEncoding.WebName,
SupportedEncodings[i].WebName,
StringComparison.OrdinalIgnoreCase))
{
return SupportedEncodings[i];
}
}
// The client specified an encoding in the content type header of the request
// but we don't understand it. In this situation we don't try to pick any other encoding
// from the list of supported encodings and read the body with that encoding.
// Instead, we return null and that will translate later on into a 415 Unsupported Media Type
// response.
return null;
}
// We want to do our best effort to read the body of the request even in the
// cases where the client doesn't send a content type header or sends a content
// type header without encoding. For that reason we pick the first encoding of the
// list of supported encodings and try to use that to read the body. This encoding
// is UTF-8 by default on our formatters, which generally is a safe choice for the
// encoding.
return SupportedEncodings[0];
}
}
}

View File

@ -1098,6 +1098,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
} }
/// <summary>
/// The list of '{0}' must not be empty. Add at least one supported encoding.
/// </summary>
internal static string TextInputFormatter_SupportedEncodingsMustNotBeEmpty
{
get { return GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"); }
}
/// <summary>
/// The list of '{0}' must not be empty. Add at least one supported encoding.
/// </summary>
internal static string FormatTextInputFormatter_SupportedEncodingsMustNotBeEmpty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"), p0);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -331,4 +331,7 @@
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve"> <data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value> <value>The field {0} must be a number.</value>
</data> </data>
<data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
</data>
</root> </root>

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -13,9 +14,9 @@ using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
{ {
/// <summary> /// <summary>
/// An <see cref="InputFormatter"/> for JSON content. /// An <see cref="TextInputFormatter"/> for JSON content.
/// </summary> /// </summary>
public class JsonInputFormatter : InputFormatter public class JsonInputFormatter : TextInputFormatter
{ {
private readonly IArrayPool<char> _charPool; private readonly IArrayPool<char> _charPool;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -116,22 +117,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Get the character encoding for the content. if (encoding == null)
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{ {
return InputFormatterResult.FailureAsync(); throw new ArgumentNullException(nameof(encoding));
} }
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
using (var streamReader = context.ReaderFactory(request.Body, effectiveEncoding)) using (var streamReader = context.ReaderFactory(request.Body, encoding))
{ {
using (var jsonReader = new JsonTextReader(streamReader)) using (var jsonReader = new JsonTextReader(streamReader))
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
@ -38,14 +39,21 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
/// <inheritdoc /> /// <inheritdoc />
public async override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public async override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var result = await base.ReadRequestBodyAsync(context); if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
var result = await base.ReadRequestBodyAsync(context, encoding);
if (!result.HasError) if (!result.HasError)
{ {
var jsonPatchDocument = (IJsonPatchDocument)result.Model; var jsonPatchDocument = (IJsonPatchDocument)result.Model;
@ -61,6 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <inheritdoc /> /// <inheritdoc />
public override bool CanRead(InputFormatterContext context) public override bool CanRead(InputFormatterContext context)
{ {
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var modelTypeInfo = context.ModelType.GetTypeInfo(); var modelTypeInfo = context.ModelType.GetTypeInfo();
if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) || if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) ||
!modelTypeInfo.IsGenericType) !modelTypeInfo.IsGenericType)

View File

@ -12,7 +12,6 @@ using System.Xml;
using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml;
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
{ {
@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// This class handles deserialization of input XML data /// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="DataContractSerializer"/>. /// to strongly-typed objects using <see cref="DataContractSerializer"/>.
/// </summary> /// </summary>
public class XmlDataContractSerializerInputFormatter : InputFormatter public class XmlDataContractSerializerInputFormatter : TextInputFormatter
{ {
private DataContractSerializerSettings _serializerSettings; private DataContractSerializerSettings _serializerSettings;
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>(); private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
@ -86,16 +85,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{ {
var effectiveEncoding = SelectCharacterEncoding(context); if (context == null)
if (effectiveEncoding == null)
{ {
return InputFormatterResult.FailureAsync(); throw new ArgumentNullException(nameof(context));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
} }
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding)) using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
{ {
var type = GetSerializableType(context.ModelType); var type = GetSerializableType(context.ModelType);
var serializer = GetCachedSerializer(type); var serializer = GetCachedSerializer(type);
@ -197,6 +200,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <returns>The <see cref="DataContractSerializer"/> instance.</returns> /// <returns>The <see cref="DataContractSerializer"/> instance.</returns>
protected virtual DataContractSerializer GetCachedSerializer(Type type) protected virtual DataContractSerializer GetCachedSerializer(Type type)
{ {
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
object serializer; object serializer;
if (!_serializerCache.TryGetValue(type, out serializer)) if (!_serializerCache.TryGetValue(type, out serializer))
{ {

View File

@ -12,7 +12,6 @@ using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml;
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
{ {
@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// This class handles deserialization of input XML data /// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="XmlSerializer"/> /// to strongly-typed objects using <see cref="XmlSerializer"/>
/// </summary> /// </summary>
public class XmlSerializerInputFormatter : InputFormatter public class XmlSerializerInputFormatter : TextInputFormatter
{ {
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>(); private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
@ -65,16 +64,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding)
{ {
var effectiveEncoding = SelectCharacterEncoding(context); if (context == null)
if (effectiveEncoding == null)
{ {
return InputFormatterResult.FailureAsync(); throw new ArgumentNullException(nameof(context));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
} }
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding)) using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
{ {
var type = GetSerializableType(context.ModelType); var type = GetSerializableType(context.ModelType);
@ -171,6 +176,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <returns>The <see cref="XmlSerializer"/> instance.</returns> /// <returns>The <see cref="XmlSerializer"/> instance.</returns>
protected virtual XmlSerializer GetCachedSerializer(Type type) protected virtual XmlSerializer GetCachedSerializer(Type type)
{ {
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
object serializer; object serializer;
if (!_serializerCache.TryGetValue(type, out serializer)) if (!_serializerCache.TryGetValue(type, out serializer))
{ {

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ActionConstraints;
@ -1455,11 +1456,13 @@ namespace Microsoft.AspNetCore.Mvc.Description
public int Id { get; set; } public int Id { get; set; }
} }
private class MockInputFormatter : InputFormatter private class MockInputFormatter : TextInputFormatter
{ {
public List<Type> SupportedTypes { get; } = new List<Type>(); public List<Type> SupportedTypes { get; } = new List<Type>();
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding effectiveEncoding)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;

View File

@ -0,0 +1,167 @@
// 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 Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
public class TextInputFormatterTest
{
[Fact]
public async Task ReadAsync_ReturnsFailure_IfItCanNotUnderstandTheContentTypeEncoding()
{
// Arrange
var formatter = new TestFormatter();
formatter.SupportedEncodings.Add(Encoding.ASCII);
var context = new InputFormatterContext(
new DefaultHttpContext(),
"something",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
context.HttpContext.Request.ContentLength = 1;
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.Equal(true, result.HasError);
Assert.Equal(true, context.ModelState.ContainsKey("something"));
Assert.Equal(1, context.ModelState["something"].Errors.Count);
var error = context.ModelState["something"].Errors[0];
Assert.IsType<UnsupportedContentTypeException>(error.Exception);
}
[Fact]
public void SelectCharacterEncoding_ThrowsInvalidOperationException_IfItDoesNotHaveAValidEncoding()
{
// Arrange
var formatter = new TestFormatter();
var context = new InputFormatterContext(
new DefaultHttpContext(),
"something",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentLength = 1;
// Act & Assert
Assert.Throws<InvalidOperationException>(() => formatter.TestSelectCharacterEncoding(context));
}
[Fact]
public void SelectCharacterEncoding_ReturnsNull_IfItCanNotUnderstandContentTypeEncoding()
{
// Arrange
var formatter = new TestFormatter();
formatter.SupportedEncodings.Add(Encoding.UTF32);
var context = new InputFormatterContext(
new DefaultHttpContext(),
"something",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
// Act
var result = formatter.TestSelectCharacterEncoding(context);
// Assert
Assert.Null(result);
}
[Fact]
public void SelectCharacterEncoding_ReturnsContentTypeEncoding_IfItCanUnderstandIt()
{
// Arrange
var formatter = new TestFormatter();
formatter.SupportedEncodings.Add(Encoding.UTF32);
formatter.SupportedEncodings.Add(Encoding.UTF8);
var context = new InputFormatterContext(
new DefaultHttpContext(),
"something",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
// Act
var result = formatter.TestSelectCharacterEncoding(context);
// Assert
Assert.Equal(Encoding.UTF8, result);
}
[Theory]
[InlineData("application/json")]
[InlineData("")]
public void SelectCharacterEncoding_ReturnsFirstEncoding_IfContentTypeIsNotSpecifiedOrDoesNotHaveEncoding(string contentType)
{
// Arrange
var formatter = new TestFormatter();
formatter.SupportedEncodings.Add(Encoding.UTF8);
formatter.SupportedEncodings.Add(Encoding.UTF32);
var context = new InputFormatterContext(
new DefaultHttpContext(),
"something",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentType = contentType;
// Act
var result = formatter.TestSelectCharacterEncoding(context);
// Assert
Assert.Equal(Encoding.UTF8, result);
}
private class TestFormatter : TextInputFormatter
{
private readonly object _object;
public TestFormatter() : this(null) { }
public TestFormatter(object @object)
{
_object = @object;
}
public IList<Type> SupportedTypes { get; } = new List<Type>();
protected override bool CanReadType(Type type)
{
return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
return InputFormatterResult.SuccessAsync(_object);
}
public Encoding TestSelectCharacterEncoding(InputFormatterContext context)
{
return SelectCharacterEncoding(context);
}
}
}
}

View File

@ -312,7 +312,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public string Name { get; set; } public string Name { get; set; }
} }
private class XyzFormatter : InputFormatter private class XyzFormatter : TextInputFormatter
{ {
public XyzFormatter() public XyzFormatter()
{ {
@ -325,7 +325,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return true; return true;
} }
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding effectiveEncoding)
{ {
throw new InvalidOperationException("Your input is bad!"); throw new InvalidOperationException("Your input is bad!");
} }

View File

@ -10,7 +10,7 @@ using Microsoft.Net.Http.Headers;
namespace FormatterWebSite namespace FormatterWebSite
{ {
public class StringInputFormatter : InputFormatter public class StringInputFormatter : TextInputFormatter
{ {
public StringInputFormatter() public StringInputFormatter()
{ {
@ -20,14 +20,8 @@ namespace FormatterWebSite
SupportedEncodings.Add(Encoding.Unicode); SupportedEncodings.Add(Encoding.Unicode);
} }
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
{ {
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{
return InputFormatterResult.FailureAsync();
}
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body, effectiveEncoding)) using (var reader = new StreamReader(request.Body, effectiveEncoding))
{ {