[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.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
@ -19,33 +14,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </summary>
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>
/// Gets the mutable collection of media type elements supported by
/// this <see cref="InputFormatter"/>.
/// </summary>
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)
{
return Activator.CreateInstance(modelType);
@ -101,6 +87,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <inheritdoc />
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.HttpContext.Request;
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>
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 />
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);
}
/// <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)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -331,4 +331,7 @@
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value>
</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>

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
@ -1455,11 +1456,13 @@ namespace Microsoft.AspNetCore.Mvc.Description
public int Id { get; set; }
}
private class MockInputFormatter : InputFormatter
private class MockInputFormatter : TextInputFormatter
{
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();
}

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Net.Http.Headers;
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; }
}
private class XyzFormatter : InputFormatter
private class XyzFormatter : TextInputFormatter
{
public XyzFormatter()
{
@ -325,7 +325,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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!");
}

View File

@ -10,7 +10,7 @@ using Microsoft.Net.Http.Headers;
namespace FormatterWebSite
{
public class StringInputFormatter : InputFormatter
public class StringInputFormatter : TextInputFormatter
{
public StringInputFormatter()
{
@ -20,14 +20,8 @@ namespace FormatterWebSite
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;
using (var reader = new StreamReader(request.Body, effectiveEncoding))
{