Adding JsonInputFormatter for reading json encoded data from the request

body
This commit is contained in:
Pranav K 2014-03-11 09:00:55 -07:00
parent 4364986137
commit 683c5bf9b3
20 changed files with 568 additions and 119 deletions

View File

@ -1,39 +0,0 @@
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Internal
{
public static class ActionBindingContextExtensions
{
public static InputFormatterContext CreateInputFormatterContext(this ActionBindingContext actionBindingContext,
ModelStateDictionary modelState,
ParameterDescriptor parameter)
{
var metadataProvider = actionBindingContext.MetadataProvider;
var parameterType = parameter.BodyParameterInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
return new InputFormatterContext(modelMetadata, modelState);
}
public static ModelBindingContext CreateModelBindingContext(this ActionBindingContext actionBindingContext,
ModelStateDictionary modelState,
ParameterDescriptor parameter)
{
var metadataProvider = actionBindingContext.MetadataProvider;
var parameterType = parameter.ParameterBindingInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
return new ModelBindingContext
{
ModelName = parameter.Name,
ModelState = modelState,
ModelMetadata = modelMetadata,
ModelBinder = actionBindingContext.ModelBinder,
ValueProvider = actionBindingContext.ValueProvider,
ValidatorProviders = actionBindingContext.ValidatorProviders,
MetadataProvider = metadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true
};
}
}
}

View File

@ -9,14 +9,14 @@ namespace Microsoft.AspNet.Mvc
IModelMetadataProvider metadataProvider,
IModelBinder modelBinder,
IValueProvider valueProvider,
IInputFormatter inputFormatter,
IInputFormatterProvider inputFormatterProvider,
IEnumerable<IModelValidatorProvider> validatorProviders)
{
ActionContext = context;
MetadataProvider = metadataProvider;
ModelBinder = modelBinder;
ValueProvider = valueProvider;
InputFormatter = inputFormatter;
InputFormatterProvider = inputFormatterProvider;
ValidatorProviders = validatorProviders;
}
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
public IValueProvider ValueProvider { get; private set; }
public IInputFormatter InputFormatter { get; private set; }
public IInputFormatterProvider InputFormatterProvider { get; private set; }
public IEnumerable<IModelValidatorProvider> ValidatorProviders { get; private set; }
}

View File

@ -10,19 +10,19 @@ namespace Microsoft.AspNet.Mvc
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IEnumerable<IModelBinder> _modelBinders;
private readonly IEnumerable<IValueProviderFactory> _valueProviderFactories;
private readonly IEnumerable<IInputFormatter> _inputFormatters;
private readonly IInputFormatterProvider _inputFormatterProvider;
private readonly IEnumerable<IModelValidatorProvider> _validatorProviders;
public DefaultActionBindingContextProvider(IModelMetadataProvider modelMetadataProvider,
IEnumerable<IModelBinder> modelBinders,
IEnumerable<IValueProviderFactory> valueProviderFactories,
IEnumerable<IInputFormatter> inputFormatters,
IInputFormatterProvider inputFormatterProvider,
IEnumerable<IModelValidatorProvider> validatorProviders)
{
_modelMetadataProvider = modelMetadataProvider;
_modelBinders = modelBinders.OrderBy(binder => binder.GetType() == typeof(ComplexModelDtoModelBinder) ? 1 : 0);
_valueProviderFactories = valueProviderFactories;
_inputFormatters = inputFormatters;
_inputFormatterProvider = inputFormatterProvider;
_validatorProviders = validatorProviders;
}
@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Mvc
_modelMetadataProvider,
new CompositeModelBinder(_modelBinders),
new CompositeValueProvider(valueProviders),
new CompositeInputFormatter(_inputFormatters),
_inputFormatterProvider,
_validatorProviders
);
}

View File

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
@ -129,24 +128,46 @@ namespace Microsoft.AspNet.Mvc
{
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(_actionContext);
var parameters = _descriptor.Parameters;
var metadataProvider = actionBindingContext.MetadataProvider;
var parameterValues = new Dictionary<string, object>(parameters.Count, StringComparer.Ordinal);
for (int i = 0; i < parameters.Count; i++)
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
if (parameter.BodyParameterInfo != null)
{
var inputFormatterContext = actionBindingContext.CreateInputFormatterContext(
modelState,
parameter);
await actionBindingContext.InputFormatter.ReadAsync(inputFormatterContext);
parameterValues[parameter.Name] = inputFormatterContext.Model;
var parameterType = parameter.BodyParameterInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
var providerContext = new InputFormatterProviderContext(actionBindingContext.ActionContext.HttpContext,
modelMetadata,
modelState);
var inputFormatter = actionBindingContext.InputFormatterProvider.GetInputFormatter(providerContext);
var formatterContext = new InputFormatterContext(actionBindingContext.ActionContext.HttpContext,
modelMetadata,
modelState);
await inputFormatter.ReadAsync(formatterContext);
parameterValues[parameter.Name] = formatterContext.Model;
}
else
{
var modelBindingContext = actionBindingContext.CreateModelBindingContext(
modelState,
parameter);
var parameterType = parameter.ParameterBindingInfo.ParameterType;
var modelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
var modelBindingContext = new ModelBindingContext
{
ModelName = parameter.Name,
ModelState = modelState,
ModelMetadata = modelMetadata,
ModelBinder = actionBindingContext.ModelBinder,
ValueProvider = actionBindingContext.ValueProvider,
ValidatorProviders = actionBindingContext.ValidatorProviders,
MetadataProvider = metadataProvider,
HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true
};
actionBindingContext.ModelBinder.BindModel(modelBindingContext);
parameterValues[parameter.Name] = modelBindingContext.Model;
}

View File

@ -1,29 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CompositeInputFormatter : IInputFormatter
{
private IInputFormatter[] _bodyReaders;
public CompositeInputFormatter(IEnumerable<IInputFormatter> bodyReaders)
{
_bodyReaders = bodyReaders.ToArray();
}
public async Task<bool> ReadAsync(InputFormatterContext context)
{
for(int i = 0; i < _bodyReaders.Length; i++)
{
if (await _bodyReaders[i].ReadAsync(context))
{
return true;
}
}
return false;
}
}
}

View File

@ -1,9 +1,25 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IInputFormatter
{
Task<bool> ReadAsync(InputFormatterContext context);
/// <summary>
/// Gets the mutable collection of media types supported by this <see cref="JsonInputFormatter"/> instance.
/// </summary>
IList<string> SupportedMediaTypes { get; }
/// <summary>
/// Gets the mutable collection of character encodings supported by this <see cref="JsonInputFormatter"/>
/// instance.
/// </summary>
IList<Encoding> SupportedEncodings { get; }
/// <summary>
/// Called during deserialization to read an object from the request.
/// </summary>
Task ReadAsync(InputFormatterContext context);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IInputFormatterProvider
{
IInputFormatter GetInputFormatter(InputFormatterProviderContext context);
}
}

View File

@ -1,15 +1,22 @@
using System;
using System.Text;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class InputFormatterContext
{
public InputFormatterContext(ModelMetadata metadata, ModelStateDictionary modelState)
public InputFormatterContext([NotNull] HttpContext httpContext,
[NotNull] ModelMetadata metadata,
[NotNull] ModelStateDictionary modelState)
{
HttpContext = httpContext;
Metadata = metadata;
ModelState = modelState;
}
public HttpContext HttpContext { get; private set; }
public ModelMetadata Metadata { get; private set; }
public ModelStateDictionary ModelState { get; private set; }

View File

@ -0,0 +1,22 @@
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class InputFormatterProviderContext
{
public InputFormatterProviderContext([NotNull] HttpContext httpContext,
[NotNull] ModelMetadata metadata,
[NotNull] ModelStateDictionary modelState)
{
HttpContext = httpContext;
Metadata = metadata;
ModelState = modelState;
}
public HttpContext HttpContext { get; private set; }
public ModelMetadata Metadata { get; private set; }
public ModelStateDictionary ModelState { get; private set; }
}
}

View File

@ -1,13 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class JsonInputFormatter : IInputFormatter
{
public Task<bool> ReadAsync(InputFormatterContext bindingContext)
private const int DefaultMaxDepth = 32;
private readonly List<Encoding> _supportedEncodings;
private readonly List<string> _supportedMediaTypes;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonInputFormatter()
{
return Task.FromResult(false);
_supportedMediaTypes = new List<string>
{
"application/json",
"text/json"
};
_supportedEncodings = new List<Encoding>
{
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true),
new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true)
};
_jsonSerializerSettings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore,
// Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions
// from deserialization errors that might occur from deeply nested objects.
MaxDepth = DefaultMaxDepth,
// Do not change this setting
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
TypeNameHandling = TypeNameHandling.None
};
}
/// <inheritdoc />
public IList<string> SupportedMediaTypes
{
get { return _supportedMediaTypes; }
}
/// <inheritdoc />
public IList<Encoding> SupportedEncodings
{
get { return _supportedEncodings; }
}
/// <summary>
/// Gets or sets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
public JsonSerializerSettings SerializerSettings
{
get { return _jsonSerializerSettings; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_jsonSerializerSettings = value;
}
}
/// <summary>
/// Gets or sets if deserialization errors are captured. When set, these errors appear in
/// the <see cref="ModelStateDictionary"/> instance of <see cref="InputFormatterContext"/>.
/// </summary>
public bool CaptureDeserilizationErrors { get; set; }
/// <inheritdoc />
public async Task ReadAsync([NotNull] InputFormatterContext context)
{
var request = context.HttpContext.Request;
if (request.ContentLength == 0)
{
var modelType = context.Metadata.ModelType;
context.Model = modelType.GetTypeInfo().IsValueType ? Activator.CreateInstance(modelType) :
null;
return ;
}
// Get the character encoding for the content
// Never non-null since SelectCharacterEncoding() throws in error / not found scenarios
var effectiveEncoding = SelectCharacterEncoding(request.GetContentType());
context.Model = 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 bool IsSupportedContentType(ContentTypeHeaderValue contentType)
{
return contentType != null &&
_supportedMediaTypes.Contains(contentType.ContentType, StringComparer.OrdinalIgnoreCase);
}
private Task<object> ReadInternal(InputFormatterContext context,
Encoding effectiveEncoding)
{
var type = context.Metadata.ModelType;
var request = context.HttpContext.Request;
using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding))
{
jsonReader.CloseInput = false;
var jsonSerializer = CreateJsonSerializer();
EventHandler<Newtonsoft.Json.Serialization.ErrorEventArgs> errorHandler = null;
if (CaptureDeserilizationErrors)
{
errorHandler = (sender, e) =>
{
var exception = e.ErrorContext.Error;
context.ModelState.AddModelError(e.ErrorContext.Path, e.ErrorContext.Error);
// Error must always be marked as handled
// Failure to do so can cause the exception to be rethrown at every recursive level and overflow the
// stack for x64 CLR processes
e.ErrorContext.Handled = true;
};
jsonSerializer.Error += errorHandler;
}
try
{
return Task.FromResult(jsonSerializer.Deserialize(jsonReader, type));
}
finally
{
// Clean up the error handler in case CreateJsonSerializer() reuses a serializer
if (errorHandler != null)
{
jsonSerializer.Error -= errorHandler;
}
}
}
}
private Encoding SelectCharacterEncoding(ContentTypeHeaderValue contentType)
{
if (contentType != null)
{
// Find encoding based on content type charset parameter
var charset = contentType.CharSet;
if (!string.IsNullOrWhiteSpace(contentType.CharSet))
{
for (var i = 0; i < _supportedEncodings.Count; i++)
{
var supportedEncoding = _supportedEncodings[i];
if (charset.Equals(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.FormatMediaTypeFormatterNoEncoding(GetType().FullName));
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class TempInputFormatterProvider : IInputFormatterProvider
{
private readonly IInputFormatter[] _formatters;
public TempInputFormatterProvider(IEnumerable<IInputFormatter> formatters)
{
_formatters = formatters.ToArray();
}
public IInputFormatter GetInputFormatter(InputFormatterProviderContext context)
{
var request = context.HttpContext.Request;
var contentType = request.GetContentType();
if (contentType == null)
{
// TODO: http exception?
throw new InvalidOperationException("400: Bad Request");
}
for (var i = 0; i < _formatters.Length; i++)
{
var formatter = _formatters[i];
if (formatter.SupportedMediaTypes.Contains(contentType.ContentType, StringComparer.OrdinalIgnoreCase))
{
return formatter;
}
}
// TODO: Http exception
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "415: Unsupported content type {0}", contentType));
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{
public class ContentTypeHeaderValue
{
public ContentTypeHeaderValue([NotNull] string contentType,
string charSet)
{
ContentType = contentType;
CharSet = charSet;
}
public string ContentType { get; private set; }
public string CharSet { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{
public static class HttpRequestExtensions
{
private const string ContentTypeHeader = "Content-Type";
private const string CharSetToken = "charset=";
public static ContentTypeHeaderValue GetContentType(this HttpRequest httpRequest)
{
var headerValue = httpRequest.Headers[ContentTypeHeader];
if (!string.IsNullOrEmpty(headerValue))
{
var tokens = headerValue.Split(new[] { ';' }, 2);
string charSet = null;
if (tokens.Length > 1 && tokens[1].TrimStart().StartsWith(CharSetToken, StringComparison.OrdinalIgnoreCase))
{
charSet = tokens[1].TrimStart().Substring(CharSetToken.Length);
}
return new ContentTypeHeaderValue(tokens[0], charSet);
}
return null;
}
}
}

View File

@ -59,6 +59,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <summary>
/// No encoding found for media type formatter '{0}'. There must be at least one supported encoding registered in order for the media type formatter to read or write content.
/// </summary>
internal static string MediaTypeFormatterNoEncoding
{
get { return GetString("MediaTypeFormatterNoEncoding"); }
}
/// <summary>
/// No encoding found for media type formatter '{0}'. There must be at least one supported encoding registered in order for the media type formatter to read or write content.
/// </summary>
internal static string FormatMediaTypeFormatterNoEncoding(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MediaTypeFormatterNoEncoding"), p0);
}
/// Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)].
/// </summary>
internal static string MissingDataMemberIsRequired

View File

@ -126,6 +126,9 @@
<data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
<value>The key is invalid JQuery syntax because it is missing a closing bracket.</value>
</data>
<data name="MediaTypeFormatterNoEncoding" xml:space="preserve">
<value>No encoding found for input formatter '{0}'. There must be at least one supported encoding registered in order for the formatter to read content.</value>
</data>
<data name="MissingDataMemberIsRequired" xml:space="preserve">
<value>Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)].</value>
</data>

View File

@ -45,54 +45,49 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return ConvertTo(type, culture: null);
}
public virtual object ConvertTo(Type type, CultureInfo culture)
public virtual object ConvertTo([NotNull] Type type, CultureInfo culture)
{
if (type == null)
{
throw Error.ArgumentNull("type");
}
TypeInfo typeInfo = type.GetTypeInfo();
object value = RawValue;
var value = RawValue;
if (value == null)
{
// treat null route parameters as though they were the default value for the type
return typeInfo.IsValueType ? Activator.CreateInstance(type) : null;
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) :
null;
}
if (value.GetType().GetTypeInfo().IsAssignableFrom(typeInfo))
if (value.GetType().IsAssignableFrom(type))
{
return value;
}
CultureInfo cultureToUse = culture ?? Culture;
var cultureToUse = culture ?? Culture;
return UnwrapPossibleArrayType(cultureToUse, value, type);
}
private static object ConvertSimpleType(CultureInfo culture, object value, TypeInfo destinationType)
private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
{
if (value == null || value.GetType().GetTypeInfo().IsAssignableFrom(destinationType))
if (value == null || value.GetType().IsAssignableFrom(destinationType))
{
return value;
}
// if this is a user-input value but the user didn't type anything, return no value
string valueAsString = value as string;
var valueAsString = value as string;
if (valueAsString != null && String.IsNullOrWhiteSpace(valueAsString))
if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString))
{
return null;
}
if (destinationType == typeof(int).GetTypeInfo())
if (destinationType == typeof(int))
{
return Convert.ToInt32(value);
}
else if (destinationType == typeof(bool).GetTypeInfo())
else if (destinationType == typeof(bool))
{
return Boolean.Parse(value.ToString());
}
else if (destinationType == typeof(string).GetTypeInfo())
else if (destinationType == typeof(string))
{
return Convert.ToString(value);
}
@ -139,25 +134,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
{
// array conversion results in four cases, as below
Array valueAsArray = value as Array;
var valueAsArray = value as Array;
if (destinationType.IsArray)
{
Type destinationElementType = destinationType.GetElementType();
TypeInfo destElementTypeInfo = destinationElementType.GetTypeInfo();
var destinationElementType = destinationType.GetElementType();
if (valueAsArray != null)
{
// case 1: both destination + source type are arrays, so convert each element
IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
for (int i = 0; i < valueAsArray.Length; i++)
for (var i = 0; i < valueAsArray.Length; i++)
{
converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destElementTypeInfo);
converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType);
}
return converted;
}
else
{
// case 2: destination type is array but source is single element, so wrap element in array + convert
object element = ConvertSimpleType(culture, value, destElementTypeInfo);
var element = ConvertSimpleType(culture, value, destinationElementType);
IList converted = Array.CreateInstance(destinationElementType, 1);
converted[0] = element;
return converted;
@ -169,7 +163,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (valueAsArray.Length > 0)
{
value = valueAsArray.GetValue(0);
return ConvertSimpleType(culture, value, destinationType.GetTypeInfo());
return ConvertSimpleType(culture, value, destinationType);
}
else
{
@ -179,7 +173,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
// case 4: both destination + source type are single elements, so convert
return ConvertSimpleType(culture, value, destinationType.GetTypeInfo());
return ConvertSimpleType(culture, value, destinationType);
}
}
}

View File

@ -24,6 +24,7 @@
"System.Diagnostics.Tools": "4.0.0.0",
"System.Dynamic.Runtime": "4.0.0.0",
"System.Globalization": "4.0.10.0",
"System.IO": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Reflection.Emit.ILGeneration": "4.0.0.0",
@ -35,9 +36,11 @@
"System.Runtime": "4.0.20.0",
"System.Runtime.Extensions": "4.0.10.0",
"System.Runtime.InteropServices": "4.0.20.0",
"System.Runtime.Serialization.Primitives": "4.0.0.0",
"System.Text.Encoding": "4.0.20.0",
"System.Text.Encoding.Extensions": "4.0.10.0",
"System.Threading": "4.0.0.0",
"System.Threading.Tasks": "4.0.0.0",
"System.Runtime.Serialization.Primitives": "4.0.0.0"
"System.Threading.Tasks": "4.0.0.0"
}
}
}

View File

@ -60,6 +60,7 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IModelBinder, ComplexModelDtoModelBinder>();
yield return describe.Transient<IInputFormatter, JsonInputFormatter>();
yield return describe.Transient<IInputFormatterProvider, TempInputFormatterProvider>();
yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>();

View File

@ -0,0 +1,142 @@
#if NET45
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class JsonInputFormatterTest
{
[Fact]
public void DefaultMediaType_ReturnsApplicationJson()
{
// Arrange
var formatter = new JsonInputFormatter();
// Act
var mediaType = formatter.SupportedMediaTypes[0];
// Assert
Assert.Equal("application/json", mediaType);
}
public static IEnumerable<object[]> JsonFormatterReadSimpleTypesData
{
get
{
yield return new object[] { "100", typeof(int), 100 };
yield return new object[] { "'abcd'", typeof(string), "abcd" };
yield return new object[] { "'2012-02-01 12:45 AM'", typeof(DateTime),
new DateTime(2012, 02, 01, 00, 45, 00) };
}
}
[Theory]
[MemberData("JsonFormatterReadSimpleTypesData")]
public async Task JsonFormatterReadsSimpleTypes(string content, Type type, object expected)
{
// Arrange
var formatter = new JsonInputFormatter();
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var modelState = new ModelStateDictionary();
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, type);
var context = new InputFormatterContext(httpContext, metadata, modelState);
// Act
await formatter.ReadAsync(context);
// Assert
Assert.Equal(expected, context.Model);
}
[Fact]
public async Task JsonFormatterReadsComplexTypes()
{
// Arrange
var content = "{name: 'Person Name', Age: '30'}";
var formatter = new JsonInputFormatter();
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var modelState = new ModelStateDictionary();
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(User));
var context = new InputFormatterContext(httpContext, metadata, modelState);
// Act
await formatter.ReadAsync(context);
// Assert
var model = Assert.IsType<User>(context.Model);
Assert.Equal("Person Name", model.Name);
Assert.Equal(30, model.Age);
}
[Fact]
public async Task ReadAsync_ThrowsOnDeserializationErrors()
{
// Arrange
var content = "{name: 'Person Name', Age: 'not-an-age'}";
var formatter = new JsonInputFormatter();
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var modelState = new ModelStateDictionary();
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(User));
var context = new InputFormatterContext(httpContext, metadata, modelState);
// Act and Assert
await Assert.ThrowsAsync<JsonReaderException>(() => formatter.ReadAsync(context));
}
[Fact]
public async Task ReadAsync_AddsModelValidationErrorsToModelState_WhenCaptureErrorsIsSet()
{
// Arrange
var content = "{name: 'Person Name', Age: 'not-an-age'}";
var formatter = new JsonInputFormatter { CaptureDeserilizationErrors = true };
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var modelState = new ModelStateDictionary();
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(User));
var context = new InputFormatterContext(httpContext, metadata, modelState);
// Act
await formatter.ReadAsync(context);
// Assert
Assert.Equal("Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 39.",
modelState["Age"].Errors[0].Exception.Message);
}
private static HttpContext GetHttpContext(byte[] contentBytes,
string contentType = "application/json")
{
var request = new Mock<HttpRequest>();
var headers = new Mock<IHeaderDictionary>();
headers.SetupGet(h => h["Content-Type"]).Returns(contentType);
request.SetupGet(r => r.Headers).Returns(headers.Object);
request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes));
var httpContext = new Mock<HttpContext>();
httpContext.SetupGet(c => c.Request).Returns(request.Object);
return httpContext.Object;
}
private sealed class User
{
public string Name { get; set; }
public decimal Age { get; set; }
}
}
}
#endif

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.PipelineCore": "0.1-alpha-*",
"Microsoft.ComponentModel.DataAnnotations" : "4.0.10.0",
"Microsoft.AspNet.Mvc.ModelBinding" : "",
"Newtonsoft.Json": "5.0.8",
"TestCommon" : "",
"Xunit.KRunner": "0.1-alpha-*",
"xunit.abstractions": "2.0.0-aspnet-*",