added InputFormatter base type

This commit is contained in:
Ajay Bhargav Baaskaran 2015-02-06 12:16:15 -08:00
parent e1e43e1e8c
commit 13e1c9d5e1
9 changed files with 256 additions and 228 deletions

View File

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Reads an object from the request body.
/// </summary>
public abstract class InputFormatter : IInputFormatter
{
/// <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 <see cref="MediaTypeHeaderValue"/> elements supported by
/// this <see cref="InputFormatter"/>.
/// </summary>
public IList<MediaTypeHeaderValue> SupportedMediaTypes { get; } = new List<MediaTypeHeaderValue>();
protected object GetDefaultValueForType(Type modelType)
{
if (modelType.GetTypeInfo().IsValueType)
{
return Activator.CreateInstance(modelType);
}
return null;
}
/// <inheritdoc />
public virtual bool CanRead(InputFormatterContext context)
{
var contentType = context.ActionContext.HttpContext.Request.ContentType;
MediaTypeHeaderValue requestContentType;
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType))
{
return false;
}
return SupportedMediaTypes
.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
}
/// <inheritdoc />
public virtual async Task<object> ReadAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
if (request.ContentLength == 0)
{
return GetDefaultValueForType(context.ModelType);
}
return await ReadRequestBodyAsync(context);
}
/// <summary>
/// Reads the request body.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/> associated with the call.</param>
/// <returns>A task which can read the request body.</returns>
public abstract Task<object> ReadRequestBodyAsync(InputFormatterContext context);
/// <summary>
/// Returns encoding based on content type charset parameter.
/// </summary>
protected Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType)
{
if (contentType != null)
{
var charset = contentType.Charset;
if (!string.IsNullOrWhiteSpace(contentType.Charset))
{
foreach (var supportedEncoding in SupportedEncodings)
{
if (string.Equals(charset, 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.
throw new InvalidOperationException(Resources.FormatInputFormatterNoEncoding(GetType().FullName));
}
}
}

View File

@ -2,29 +2,24 @@
// 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.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonInputFormatter : IInputFormatter
public class JsonInputFormatter : InputFormatter
{
private const int DefaultMaxDepth = 32;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonInputFormatter()
{
SupportedEncodings = new List<Encoding>();
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian);
SupportedMediaTypes = new List<MediaTypeHeaderValue>();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json"));
@ -42,12 +37,6 @@ namespace Microsoft.AspNet.Mvc
};
}
/// <inheritdoc />
public IList<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
/// <inheritdoc />
public IList<Encoding> SupportedEncodings { get; private set; }
/// <summary>
/// Gets or sets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
@ -72,31 +61,10 @@ namespace Microsoft.AspNet.Mvc
public bool CaptureDeserilizationErrors { get; set; }
/// <inheritdoc />
public bool CanRead(InputFormatterContext context)
{
var contentType = context.ActionContext.HttpContext.Request.ContentType;
MediaTypeHeaderValue requestContentType;
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType))
{
return false;
}
return SupportedMediaTypes
.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
}
/// <inheritdoc />
public async Task<object> ReadAsync([NotNull] InputFormatterContext context)
public override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
{
var type = context.ModelType;
var request = context.ActionContext.HttpContext.Request;
if (request.ContentLength == 0)
{
var modelType = context.ModelType;
var model = modelType.GetTypeInfo().IsValueType ? Activator.CreateInstance(modelType) :
null;
return model;
}
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
@ -104,38 +72,6 @@ namespace Microsoft.AspNet.Mvc
// Never non-null since SelectCharacterEncoding() throws in error / not found scenarios
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
return await ReadInternal(context, effectiveEncoding);
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonReader"/>.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/> for the read.</param>
/// <param name="readStream">The <see cref="Stream"/> from which to read.</param>
/// <param name="effectiveEncoding">The <see cref="Encoding"/> to use when reading.</param>
/// <returns>The <see cref="JsonReader"/> used during deserialization.</returns>
public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context,
[NotNull] Stream readStream,
[NotNull] Encoding effectiveEncoding)
{
return new JsonTextReader(new StreamReader(readStream, effectiveEncoding));
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonSerializer"/>.
/// </summary>
/// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
public virtual JsonSerializer CreateJsonSerializer()
{
return JsonSerializer.Create(SerializerSettings);
}
private Task<object> ReadInternal(InputFormatterContext context,
Encoding effectiveEncoding)
{
var type = context.ModelType;
var request = context.ActionContext.HttpContext.Request;
using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding))
{
jsonReader.CloseInput = false;
@ -172,31 +108,27 @@ namespace Microsoft.AspNet.Mvc
}
}
private Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType)
/// <summary>
/// Called during deserialization to get the <see cref="JsonReader"/>.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/> for the read.</param>
/// <param name="readStream">The <see cref="Stream"/> from which to read.</param>
/// <param name="effectiveEncoding">The <see cref="Encoding"/> to use when reading.</param>
/// <returns>The <see cref="JsonReader"/> used during deserialization.</returns>
public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context,
[NotNull] Stream readStream,
[NotNull] Encoding effectiveEncoding)
{
if (contentType != null)
{
// Find encoding based on content type charset parameter
var charset = contentType.Charset;
if (!string.IsNullOrWhiteSpace(contentType.Charset))
{
foreach (var supportedEncoding in SupportedEncodings)
{
if (string.Equals(charset, supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase))
{
return supportedEncoding;
}
}
}
}
return new JsonTextReader(new StreamReader(readStream, effectiveEncoding));
}
if (SupportedEncodings.Count > 0)
{
return SupportedEncodings[0];
}
// No supported encoding was found so there is no way for us to start reading.
throw new InvalidOperationException(Resources.FormatInputFormatterNoEncoding(GetType().FullName));
/// <summary>
/// Called during deserialization to get the <see cref="JsonSerializer"/>.
/// </summary>
/// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
public virtual JsonSerializer CreateJsonSerializer()
{
return JsonSerializer.Create(SerializerSettings);
}
}
}

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc
/// this <see cref="OutputFormatter"/>. The encodings are
/// used when writing the data.
/// </summary>
public IList<Encoding> SupportedEncodings { get; private set; }
public IList<Encoding> SupportedEncodings { get; }
/// <summary>
/// Gets the mutable collection of <see cref="MediaTypeHeaderValue"/> elements supported by

View File

@ -4,10 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNet.Mvc.Internal;
@ -19,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Xml
/// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="DataContractSerializer"/>.
/// </summary>
public class XmlDataContractSerializerInputFormatter : IInputFormatter
public class XmlDataContractSerializerInputFormatter : InputFormatter
{
private DataContractSerializerSettings _serializerSettings;
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
@ -29,11 +26,9 @@ namespace Microsoft.AspNet.Mvc.Xml
/// </summary>
public XmlDataContractSerializerInputFormatter()
{
SupportedEncodings = new List<Encoding>();
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian);
SupportedMediaTypes = new List<MediaTypeHeaderValue>();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
@ -49,12 +44,6 @@ namespace Microsoft.AspNet.Mvc.Xml
/// </summary>
public IList<IWrapperProviderFactory> WrapperProviderFactories { get; }
/// <inheritdoc />
public IList<MediaTypeHeaderValue> SupportedMediaTypes { get; }
/// <inheritdoc />
public IList<Encoding> SupportedEncodings { get; }
/// <summary>
/// Indicates the acceptable input XML depth.
/// </summary>
@ -73,20 +62,6 @@ namespace Microsoft.AspNet.Mvc.Xml
get { return _readerQuotas; }
}
/// <inheritdoc />
public bool CanRead(InputFormatterContext context)
{
var contentType = context.ActionContext.HttpContext.Request.ContentType;
MediaTypeHeaderValue requestContentType;
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType))
{
return false;
}
return SupportedMediaTypes
.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
}
/// <summary>
/// Gets or sets the <see cref="DataContractSerializerSettings"/> used to configure the
/// <see cref="DataContractSerializer"/>.
@ -110,15 +85,30 @@ namespace Microsoft.AspNet.Mvc.Xml
/// </summary>
/// <param name="context">The input formatter context which contains the body to be read.</param>
/// <returns>Task which reads the input.</returns>
public async Task<object> ReadAsync(InputFormatterContext context)
public override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
if (request.ContentLength == 0)
{
return GetDefaultValueForType(context.ModelType);
}
return await ReadInternalAsync(context);
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body)))
{
var type = GetSerializableType(context.ModelType);
var serializer = CreateSerializer(type);
var deserializedObject = serializer.ReadObject(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
var unwrappable = deserializedObject as IUnwrappable;
if (unwrappable != null)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return Task.FromResult(deserializedObject);
}
}
/// <summary>
@ -154,41 +144,5 @@ namespace Microsoft.AspNet.Mvc.Xml
{
return new DataContractSerializer(type, _serializerSettings);
}
private object GetDefaultValueForType(Type modelType)
{
if (modelType.GetTypeInfo().IsValueType)
{
return Activator.CreateInstance(modelType);
}
return null;
}
private Task<object> ReadInternalAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body)))
{
var type = GetSerializableType(context.ModelType);
var serializer = CreateSerializer(type);
var deserializedObject = serializer.ReadObject(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
var unwrappable = deserializedObject as IUnwrappable;
if (unwrappable != null)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return Task.FromResult(deserializedObject);
}
}
}
}

View File

@ -4,9 +4,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
@ -19,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Xml
/// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="XmlSerializer"/>
/// </summary>
public class XmlSerializerInputFormatter : IInputFormatter
public class XmlSerializerInputFormatter : InputFormatter
{
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
@ -28,11 +25,9 @@ namespace Microsoft.AspNet.Mvc.Xml
/// </summary>
public XmlSerializerInputFormatter()
{
SupportedEncodings = new List<Encoding>();
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian);
SupportedMediaTypes = new List<MediaTypeHeaderValue>();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
@ -46,12 +41,6 @@ namespace Microsoft.AspNet.Mvc.Xml
/// </summary>
public IList<IWrapperProviderFactory> WrapperProviderFactories { get; }
/// <inheritdoc />
public IList<MediaTypeHeaderValue> SupportedMediaTypes { get; }
/// <inheritdoc />
public IList<Encoding> SupportedEncodings { get; }
/// <summary>
/// Indicates the acceptable input XML depth.
/// </summary>
@ -70,34 +59,35 @@ namespace Microsoft.AspNet.Mvc.Xml
get { return _readerQuotas; }
}
/// <inheritdoc />
public bool CanRead(InputFormatterContext context)
{
var contentType = context.ActionContext.HttpContext.Request.ContentType;
MediaTypeHeaderValue requestContentType;
if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType))
{
return false;
}
return SupportedMediaTypes
.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
}
/// <summary>
/// Reads the input XML.
/// </summary>
/// <param name="context">The input formatter context which contains the body to be read.</param>
/// <returns>Task which reads the input.</returns>
public async Task<object> ReadAsync(InputFormatterContext context)
public override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
if (request.ContentLength == 0)
{
return GetDefaultValueForType(context.ModelType);
}
return await ReadInternalAsync(context);
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body)))
{
var type = GetSerializableType(context.ModelType);
var serializer = CreateSerializer(type);
var deserializedObject = serializer.Deserialize(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
var unwrappable = deserializedObject as IUnwrappable;
if (unwrappable != null)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return Task.FromResult(deserializedObject);
}
}
/// <summary>
@ -132,41 +122,5 @@ namespace Microsoft.AspNet.Mvc.Xml
{
return new XmlSerializer(type);
}
private object GetDefaultValueForType(Type modelType)
{
if (modelType.GetTypeInfo().IsValueType)
{
return Activator.CreateInstance(modelType);
}
return null;
}
private Task<object> ReadInternalAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body)))
{
var type = GetSerializableType(context.ModelType);
var serializer = CreateSerializer(type);
var deserializedObject = serializer.Deserialize(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
var unwrappable = deserializedObject as IUnwrappable;
if (unwrappable != null)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return Task.FromResult(deserializedObject);
}
}
}
}

View File

@ -163,5 +163,42 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
}
[Theory]
[InlineData("utf-8")]
[InlineData("unicode")]
public async Task CustomFormatter_IsSelected_ForSupportedContentTypeAndEncoding(string encoding)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var content = new StringContent("Test Content", Encoding.GetEncoding(encoding), "text/plain");
// Act
var response = await client.PostAsync("http://localhost/InputFormatter/ReturnInput/", content);
var responseBody = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Test Content", responseBody);
}
[Theory]
[InlineData("image/png")]
[InlineData("image/jpeg")]
public async Task CustomFormatter_NotSelected_ForUnsupportedContentType(string contentType)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var content = new StringContent("Test Content", Encoding.UTF8, contentType);
// Act
var response = await client.PostAsync("http://localhost/InputFormatter/ReturnInput/", content);
var responseBody = await response.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
}

View File

@ -3,6 +3,7 @@
using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.WebUtilities;
namespace FormatterWebSite.Controllers
{
@ -35,5 +36,15 @@ namespace FormatterWebSite.Controllers
{
return dummy;
}
public IActionResult ReturnInput([FromBody] string test)
{
if (!ModelState.IsValid)
{
return new HttpStatusCodeResult(StatusCodes.Status400BadRequest);
}
return Content(test);
}
}
}

View File

@ -25,6 +25,7 @@ namespace FormatterWebSite
options.ValidationExcludeFilters.Add(typeof(Supplier));
options.AddXmlDataContractSerializerFormatter();
options.InputFormatters.Add(new StringInputFormatter());
});
});

View File

@ -0,0 +1,34 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.Net.Http.Headers;
namespace FormatterWebSite
{
public class StringInputFormatter : InputFormatter
{
public StringInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
public override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.ActionContext.HttpContext.Request;
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
using (var reader = new StreamReader(request.Body, effectiveEncoding))
{
var stringContent = reader.ReadToEnd();
return Task.FromResult<object>(stringContent);
}
}
}
}