Adding JsonInputFormatter for reading json encoded data from the request
body
This commit is contained in:
parent
4364986137
commit
683c5bf9b3
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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-*",
|
||||
|
|
|
|||
Loading…
Reference in New Issue