[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:
parent
8e8de413f8
commit
fe639f028f
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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!");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue