Add `InputFormatterResult` and `InputFormatterContext.ModelName`

- #2722
- make communication of errors from formatters to `BodyModelBinder` explicit
  - `JsonInputFormatter` now adds errors to `ModelStateDictionary` with correct key
- change `InputFormatter.SelectCharacterEncoding()` to add an error and return `null` when it fails
  - one less `Exception` case and removes some duplicate code

nits:
- improve some doc comments (more `<inheritdoc/>`, `<paramref/>` and `<see/>`)
- add another two `BodyValidationIntegrationTests` tests
This commit is contained in:
Doug Bunting 2015-09-19 21:36:38 -07:00
parent 0476d53f1d
commit 42017faa21
16 changed files with 557 additions and 210 deletions

View File

@ -11,20 +11,21 @@ namespace Microsoft.AspNet.Mvc.Formatters
public interface IInputFormatter
{
/// <summary>
/// Determines whether this <see cref="IInputFormatter"/> can de-serialize
/// an object of the specified type.
/// Determines whether this <see cref="IInputFormatter"/> can deserialize an object of the
/// <paramref name="context"/>'s <see cref="InputFormatterContext.ModelType"/>.
/// </summary>
/// <param name="context">Input formatter context associated with this call.</param>
/// <returns>True if this <see cref="IInputFormatter"/> supports the passed in
/// request's content-type and is able to de-serialize the request body.
/// False otherwise.</returns>
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>
/// <c>true</c> if this <see cref="IInputFormatter"/> can deserialize an object of the
/// <paramref name="context"/>'s <see cref="InputFormatterContext.ModelType"/>. <c>false</c> otherwise.
/// </returns>
bool CanRead(InputFormatterContext context);
/// <summary>
/// Called during deserialization to read an object from the request.
/// Reads an object from the request body.
/// </summary>
/// <param name="context">Input formatter context associated with this call.</param>
/// <returns>A task that deserializes the request body.</returns>
Task<object> ReadAsync(InputFormatterContext context);
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
Task<InputFormatterResult> ReadAsync(InputFormatterContext context);
}
}

View File

@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
/// <param name="httpContext">
/// The <see cref="Http.HttpContext"/> for the current operation.
/// </param>
/// <param name="modelName">The name of the model.</param>
/// <param name="modelState">
/// The <see cref="ModelStateDictionary"/> for recording errors.
/// </param>
@ -27,10 +28,12 @@ namespace Microsoft.AspNet.Mvc.Formatters
/// </param>
public InputFormatterContext(
[NotNull] HttpContext httpContext,
[NotNull] string modelName,
[NotNull] ModelStateDictionary modelState,
[NotNull] Type modelType)
{
HttpContext = httpContext;
ModelName = modelName;
ModelState = modelState;
ModelType = modelType;
}
@ -38,7 +41,12 @@ namespace Microsoft.AspNet.Mvc.Formatters
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> associated with the current operation.
/// </summary>
public HttpContext HttpContext { get; private set; }
public HttpContext HttpContext { get; }
/// <summary>
/// Gets the name of the model. Used as the key or key prefix for errors added to <see cref="ModelState"/>.
/// </summary>
public string ModelName { get; }
/// <summary>
/// Gets the <see cref="ModelStateDictionary"/> associated with the current operation.
@ -48,6 +56,6 @@ namespace Microsoft.AspNet.Mvc.Formatters
/// <summary>
/// Gets the expected <see cref="Type"/> of the model represented by the request body.
/// </summary>
public Type ModelType { get; private set; }
public Type ModelType { get; }
}
}

View File

@ -0,0 +1,93 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Formatters
{
/// <summary>
/// Result of a <see cref="IInputFormatter.ReadAsync"/> operation.
/// </summary>
public class InputFormatterResult
{
private static readonly InputFormatterResult _failure = new InputFormatterResult();
private static readonly Task<InputFormatterResult> _failureAsync = Task.FromResult(_failure);
private InputFormatterResult()
{
HasError = true;
}
private InputFormatterResult(object model)
{
Model = model;
}
/// <summary>
/// Gets an indication whether the <see cref="IInputFormatter.ReadAsync"/> operation had an error.
/// </summary>
public bool HasError { get; }
/// <summary>
/// Gets the deserialized <see cref="object"/>.
/// </summary>
/// <value>
/// <c>null</c> if <see cref="HasError"/> is <c>true</c>.
/// </value>
public object Model { get; }
/// <summary>
/// Returns an <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
/// operation failed.
/// </summary>
/// <returns>
/// An <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
/// operation failed i.e. with <see cref="HasError"/> <c>true</c>.
/// </returns>
public static InputFormatterResult Failure()
{
return _failure;
}
/// <summary>
/// Returns a <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating
/// the <see cref="IInputFormatter.ReadAsync"/> operation failed.
/// </summary>
/// <returns>
/// A <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating the
/// <see cref="IInputFormatter.ReadAsync"/> operation failed i.e. with <see cref="HasError"/> <c>true</c>.
/// </returns>
public static Task<InputFormatterResult> FailureAsync()
{
return _failureAsync;
}
/// <summary>
/// Returns an <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
/// operation was successful.
/// </summary>
/// <param name="model">The deserialized <see cref="object"/>.</param>
/// <returns>
/// An <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
/// operation succeeded i.e. with <see cref="HasError"/> <c>false</c>.
/// </returns>
public static InputFormatterResult Success(object model)
{
return new InputFormatterResult(model);
}
/// <summary>
/// Returns a <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating
/// the <see cref="IInputFormatter.ReadAsync"/> operation was successful.
/// </summary>
/// <param name="model">The deserialized <see cref="object"/>.</param>
/// <returns>
/// A <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating the
/// <see cref="IInputFormatter.ReadAsync"/> operation succeeded i.e. with <see cref="HasError"/> <c>false</c>.
/// </returns>
public static Task<InputFormatterResult> SuccessAsync(object model)
{
return Task.FromResult(Success(model));
}
}
}

View File

@ -67,48 +67,58 @@ namespace Microsoft.AspNet.Mvc.Formatters
return false;
}
return SupportedMediaTypes
.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
return SupportedMediaTypes.Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType));
}
/// <summary>
/// Returns a value indicating whether or not the given type can be read by this serializer.
/// Determines whether this <see cref="InputFormatter"/> can deserialize an object of the given
/// <paramref name="type"/>.
/// </summary>
/// <param name="type">The type of object that will be read.</param>
/// <returns><c>true</c> if the type can be read, otherwise <c>false</c>.</returns>
/// <param name="type">The <see cref="Type"/> of object that will be read.</param>
/// <returns><c>true</c> if the <paramref name="type"/> can be read, otherwise <c>false</c>.</returns>
protected virtual bool CanReadType(Type type)
{
return true;
}
/// <inheritdoc />
public virtual Task<object> ReadAsync(InputFormatterContext context)
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
if (request.ContentLength == 0)
{
return Task.FromResult(GetDefaultValueForType(context.ModelType));
return InputFormatterResult.SuccessAsync(GetDefaultValueForType(context.ModelType));
}
return ReadRequestBodyAsync(context);
}
/// <summary>
/// Reads the request body.
/// Reads an object from 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);
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
/// <summary>
/// Returns encoding based on content type charset parameter.
/// Returns an <see cref="Encoding"/> based on <paramref name="context"/>'s
/// <see cref="MediaTypeHeaderValue.Charset"/>.
/// </summary>
protected Encoding SelectCharacterEncoding(MediaTypeHeaderValue contentType)
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
/// <returns>
/// An <see cref="Encoding"/> based on <paramref name="context"/>'s
/// <see cref="MediaTypeHeaderValue.Charset"/>. <c>null</c> if no supported encoding was found.
/// </returns>
protected Encoding SelectCharacterEncoding(InputFormatterContext context)
{
var request = context.HttpContext.Request;
MediaTypeHeaderValue contentType;
MediaTypeHeaderValue.TryParse(request.ContentType, out contentType);
if (contentType != null)
{
var charset = contentType.Charset;
if (!string.IsNullOrWhiteSpace(contentType.Charset))
if (!string.IsNullOrWhiteSpace(charset))
{
foreach (var supportedEncoding in SupportedEncodings)
{
@ -126,7 +136,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
// No supported encoding was found so there is no way for us to start reading.
throw new InvalidOperationException(Resources.FormatInputFormatterNoEncoding(GetType().FullName));
context.ModelState.TryAddModelError(
context.ModelName,
Resources.FormatInputFormatterNoEncoding(GetType().FullName));
return null;
}
}
}

View File

@ -53,6 +53,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var formatterContext = new InputFormatterContext(
httpContext,
modelBindingKey,
bindingContext.ModelState,
bindingContext.ModelType);
var formatters = bindingContext.OperationBindingContext.InputFormatters;
@ -73,14 +74,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
try
{
var previousCount = bindingContext.ModelState.ErrorCount;
var model = await formatter.ReadAsync(formatterContext);
var result = await formatter.ReadAsync(formatterContext);
var model = result.Model;
// Ensure a "modelBindingKey" entry exists whether or not formatting was successful.
bindingContext.ModelState.SetModelValue(modelBindingKey, rawValue: model, attemptedValue: null);
if (bindingContext.ModelState.ErrorCount != previousCount)
if (result.HasError)
{
// Formatter added an error. Do not use the model it returned. As above, tell the model binding
// system to skip other model binders and never to fall back.
// Formatter encountered an error. Do not use the model it returned. As above, tell the model
// binding system to skip other model binders and never to fall back.
return ModelBindingResult.Failed(modelBindingKey);
}

View File

@ -49,48 +49,74 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
/// <inheritdoc />
public override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
public override Task<InputFormatterResult> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
{
var type = context.ModelType;
// Get the character encoding for the content.
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{
return InputFormatterResult.FailureAsync();
}
var request = context.HttpContext.Request;
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
// Get the character encoding for the content
// Never non-null since SelectCharacterEncoding() throws in error / not found scenarios
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding))
{
jsonReader.CloseInput = false;
var jsonSerializer = CreateJsonSerializer();
EventHandler<Newtonsoft.Json.Serialization.ErrorEventArgs> errorHandler = null;
errorHandler = (sender, e) =>
var successful = true;
EventHandler<Newtonsoft.Json.Serialization.ErrorEventArgs> errorHandler = (sender, eventArgs) =>
{
var exception = e.ErrorContext.Error;
context.ModelState.TryAddModelError(e.ErrorContext.Path, e.ErrorContext.Error);
successful = false;
var exception = eventArgs.ErrorContext.Error;
// Handle path combinations such as "" + "Property", "Parent" + "Property", or "Parent" + "[12]".
var key = eventArgs.ErrorContext.Path;
if (!string.IsNullOrEmpty(context.ModelName))
{
if (string.IsNullOrEmpty(eventArgs.ErrorContext.Path))
{
key = context.ModelName;
}
else if (eventArgs.ErrorContext.Path[0] == '[')
{
key = context.ModelName + eventArgs.ErrorContext.Path;
}
else
{
key = context.ModelName + "." + eventArgs.ErrorContext.Path;
}
}
context.ModelState.TryAddModelError(key, eventArgs.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;
eventArgs.ErrorContext.Handled = true;
};
var type = context.ModelType;
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Error += errorHandler;
object model;
try
{
return Task.FromResult(jsonSerializer.Deserialize(jsonReader, type));
model = jsonSerializer.Deserialize(jsonReader, type);
}
finally
{
// Clean up the error handler in case CreateJsonSerializer() reuses a serializer
if (errorHandler != null)
{
jsonSerializer.Error -= errorHandler;
}
jsonSerializer.Error -= errorHandler;
}
if (successful)
{
return InputFormatterResult.SuccessAsync(model);
}
return InputFormatterResult.FailureAsync();
}
}

View File

@ -27,15 +27,19 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
/// <inheritdoc />
public async override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
public async override Task<InputFormatterResult> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
{
var jsonPatchDocument = (IJsonPatchDocument)(await base.ReadRequestBodyAsync(context));
if (jsonPatchDocument != null && SerializerSettings.ContractResolver != null)
var result = await base.ReadRequestBodyAsync(context);
if (!result.HasError)
{
jsonPatchDocument.ContractResolver = SerializerSettings.ContractResolver;
var jsonPatchDocument = (IJsonPatchDocument)result.Model;
if (jsonPatchDocument != null && SerializerSettings.ContractResolver != null)
{
jsonPatchDocument.ContractResolver = SerializerSettings.ContractResolver;
}
}
return (object)jsonPatchDocument;
return result;
}
/// <inheritdoc />

View File

@ -86,19 +86,16 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
}
/// <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 override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
/// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{
return InputFormatterResult.FailureAsync();
}
var request = context.HttpContext.Request;
MediaTypeHeaderValue requestContentType;
MediaTypeHeaderValue.TryParse(request.ContentType , out requestContentType);
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding))
{
var type = GetSerializableType(context.ModelType);
@ -116,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
}
return Task.FromResult(deserializedObject);
return InputFormatterResult.SuccessAsync(deserializedObject);
}
}
@ -145,7 +142,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
protected virtual Type GetSerializableType([NotNull] Type declaredType)
{
var wrapperProvider = WrapperProviderFactories.GetWrapperProvider(
new WrapperProviderContext(declaredType, isSerialization: false));
new WrapperProviderContext(declaredType, isSerialization: false));
return wrapperProvider?.WrappingType ?? declaredType;
}

View File

@ -65,19 +65,16 @@ namespace Microsoft.AspNet.Mvc.Formatters
get { return _readerQuotas; }
}
/// <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 override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
/// <inheritdoc />
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{
return InputFormatterResult.FailureAsync();
}
var request = context.HttpContext.Request;
MediaTypeHeaderValue requestContentType;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding))
{
var type = GetSerializableType(context.ModelType);
@ -96,7 +93,7 @@ namespace Microsoft.AspNet.Mvc.Formatters
}
}
return Task.FromResult(deserializedObject);
return InputFormatterResult.SuccessAsync(deserializedObject);
}
}

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Returns(true)
.Verifiable();
mockInputFormatter.Setup(o => o.ReadAsync(It.IsAny<InputFormatterContext>()))
.Returns(Task.FromResult<object>(new Person()))
.Returns(InputFormatterResult.SuccessAsync(new Person()))
.Verifiable();
var inputFormatter = mockInputFormatter.Object;
@ -305,7 +305,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return true;
}
public override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
throw new InvalidOperationException("Your input is bad!");
}
@ -325,9 +325,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return _canRead;
}
public Task<object> ReadAsync(InputFormatterContext context)
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
return Task.FromResult<object>(this);
return InputFormatterResult.SuccessAsync(this);
}
}
}

View File

@ -38,7 +38,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
var contentBytes = Encoding.UTF8.GetBytes("content");
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
var formatterContext = new InputFormatterContext(httpContext, new ModelStateDictionary(), typeof(string));
var formatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(string));
// Act
var result = formatter.CanRead(formatterContext);
@ -80,13 +84,18 @@ namespace Microsoft.AspNet.Mvc.Formatters
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, new ModelStateDictionary(), type);
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: type);
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.Equal(expected, model);
Assert.False(result.HasError);
Assert.Equal(expected, result.Model);
}
[Fact]
@ -98,13 +107,18 @@ namespace Microsoft.AspNet.Mvc.Formatters
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, new ModelStateDictionary(), typeof(User));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: typeof(User));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
var userModel = Assert.IsType<User>(model);
Assert.False(result.HasError);
var userModel = Assert.IsType<User>(result.Model);
Assert.Equal("Person Name", userModel.Name);
Assert.Equal(30, userModel.Age);
}
@ -119,13 +133,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, modelState, typeof(User));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(User));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.True(result.HasError);
Assert.Equal(
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 39.",
modelState["Age"].Errors[0].Exception.Message);
@ -141,17 +159,21 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, modelState, typeof(User));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(User));
modelState.MaxAllowedErrors = 3;
modelState.AddModelError("key1", "error1");
modelState.AddModelError("key2", "error2");
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.True(result.HasError);
Assert.False(modelState.ContainsKey("age"));
var error = Assert.Single(modelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
@ -193,13 +215,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-8");
var inputFormatterContext = new InputFormatterContext(httpContext, modelState, typeof(UserLogin));
var inputFormatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(UserLogin));
// Act
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
var result = await jsonFormatter.ReadAsync(inputFormatterContext);
// Assert
Assert.True(result.HasError);
Assert.False(modelState.IsValid);
var modelErrorMessage = modelState.Values.First().Errors[0].Exception.Message;
@ -222,13 +248,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-8");
var inputFormatterContext = new InputFormatterContext(httpContext, modelState, typeof(UserLogin));
var inputFormatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(UserLogin));
// Act
var obj = await jsonFormatter.ReadAsync(inputFormatterContext);
var result = await jsonFormatter.ReadAsync(inputFormatterContext);
// Assert
Assert.True(result.HasError);
Assert.False(modelState.IsValid);
var modelErrorMessage = modelState.Values.First().Errors[0].Exception.Message;

View File

@ -25,13 +25,18 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, modelState, typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(JsonPatchDocument<Customer>));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(model);
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(result.Model);
Assert.Equal("add", patchDoc.Operations[0].op);
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
Assert.Equal("John", patchDoc.Operations[0].value);
@ -48,13 +53,18 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes);
var context = new InputFormatterContext(httpContext, modelState, typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(JsonPatchDocument<Customer>));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(model);
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(result.Model);
Assert.Equal("add", patchDoc.Operations[0].op);
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
Assert.Equal("John", patchDoc.Operations[0].value);
@ -78,8 +88,9 @@ namespace Microsoft.AspNet.Mvc.Formatters
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
var formatterContext = new InputFormatterContext(
httpContext,
modelState,
typeof(JsonPatchDocument<Customer>));
modelName: string.Empty,
modelState: modelState,
modelType: typeof(JsonPatchDocument<Customer>));
// Act
var result = formatter.CanRead(formatterContext);
@ -100,7 +111,11 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: "application/json-patch+json");
var formatterContext = new InputFormatterContext(httpContext, modelState, modelType);
var formatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: modelType);
// Act
var result = formatter.CanRead(formatterContext);
@ -122,13 +137,17 @@ namespace Microsoft.AspNet.Mvc.Formatters
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: "application/json-patch+json");
var context = new InputFormatterContext(httpContext, modelState, typeof(Customer));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(Customer));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.True(result.HasError);
Assert.Contains(exceptionMessage, modelState[""].Errors[0].Exception.Message);
}

View File

@ -73,7 +73,11 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
var formatterContext = new InputFormatterContext(httpContext, modelState, typeof(string));
var formatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(string));
// Act
var result = formatter.CanRead(formatterContext);
@ -146,15 +150,15 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelOne>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
var levelOneModel = model as TestLevelOne;
Assert.Equal(expectedInt, levelOneModel.SampleInt);
Assert.Equal(expectedString, levelOneModel.sampleString);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
}
[ConditionalFact]
@ -177,16 +181,16 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelTwo>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelTwo>(result.Model);
var levelTwoModel = model as TestLevelTwo;
Assert.Equal(expectedLevelTwoString, levelTwoModel.SampleString);
Assert.Equal(expectedInt, levelTwoModel.TestOne.SampleInt);
Assert.Equal(expectedString, levelTwoModel.TestOne.sampleString);
Assert.Equal(expectedLevelTwoString, model.SampleString);
Assert.Equal(expectedInt, model.TestOne.SampleInt);
Assert.Equal(expectedString, model.TestOne.sampleString);
}
[ConditionalFact]
@ -206,13 +210,13 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<DummyClass>(model);
var dummyModel = model as DummyClass;
Assert.Equal(expectedInt, dummyModel.SampleInt);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<DummyClass>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
}
[ConditionalFact]
@ -276,10 +280,12 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.NotNull(result);
Assert.False(result.HasError);
Assert.NotNull(result.Model);
Assert.True(context.HttpContext.Request.Body.CanRead);
}
@ -331,7 +337,11 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(inputBytes, contentType: "application/xml; charset=utf-16");
var context = new InputFormatterContext(httpContext, modelState, typeof(TestLevelOne));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(TestLevelOne));
// Act
var ex = await Assert.ThrowsAsync(expectedException, () => formatter.ReadAsync(context));
@ -361,14 +371,15 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
var levelTwoModel = model as TestLevelTwo;
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelTwo>(result.Model);
Buffer.BlockCopy(sampleStringBytes, 0, expectedBytes, 0, sampleStringBytes.Length);
Buffer.BlockCopy(bom, 0, expectedBytes, sampleStringBytes.Length, bom.Length);
Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(levelTwoModel.SampleString));
Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(model.SampleString));
}
[ConditionalFact]
@ -390,18 +401,22 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: "application/xml; charset=utf-16");
var context = new InputFormatterContext(httpContext, modelState, typeof(TestLevelOne));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(TestLevelOne));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelOne>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
var levelOneModel = model as TestLevelOne;
Assert.Equal(expectedInt, levelOneModel.SampleInt);
Assert.Equal(expectedString, levelOneModel.sampleString);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
}
[ConditionalFact]
@ -455,12 +470,13 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
var dummyModel = Assert.IsType<DummyClass>(model);
Assert.Equal(expectedInt, dummyModel.SampleInt);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<DummyClass>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
}
[ConditionalFact]
@ -515,19 +531,24 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
var dummyModel = Assert.IsType<SomeDummyClass>(model);
Assert.Equal(expectedInt, dummyModel.SampleInt);
Assert.Equal(expectedString, dummyModel.SampleString);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<SomeDummyClass>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.SampleString);
}
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
{
var httpContext = GetHttpContext(contentBytes);
return new InputFormatterContext(httpContext, new ModelStateDictionary(), modelType);
return new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: modelType);
}
private static HttpContext GetHttpContext(

View File

@ -59,7 +59,11 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
var formatterContext = new InputFormatterContext(httpContext, modelState, typeof(string));
var formatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(string));
// Act
var result = formatter.CanRead(formatterContext);
@ -148,17 +152,18 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelOne));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelOne>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
var levelOneModel = model as TestLevelOne;
Assert.Equal(expectedInt, levelOneModel.SampleInt);
Assert.Equal(expectedString, levelOneModel.sampleString);
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
levelOneModel.SampleDate);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
Assert.Equal(
XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
model.SampleDate);
}
[Fact]
@ -181,18 +186,19 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelTwo>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelTwo>(result.Model);
var levelTwoModel = model as TestLevelTwo;
Assert.Equal(expectedLevelTwoString, levelTwoModel.SampleString);
Assert.Equal(expectedInt, levelTwoModel.TestOne.SampleInt);
Assert.Equal(expectedString, levelTwoModel.TestOne.sampleString);
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
levelTwoModel.TestOne.SampleDate);
Assert.Equal(expectedLevelTwoString, model.SampleString);
Assert.Equal(expectedInt, model.TestOne.SampleInt);
Assert.Equal(expectedString, model.TestOne.sampleString);
Assert.Equal(
XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc),
model.TestOne.SampleDate);
}
[Fact]
@ -208,15 +214,14 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var contentBytes = Encoding.UTF8.GetBytes(input);
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<DummyClass>(model);
var dummyModel = model as DummyClass;
Assert.Equal(expectedInt, dummyModel.SampleInt);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<DummyClass>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
}
[ConditionalFact]
@ -282,10 +287,12 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.NotNull(result);
Assert.False(result.HasError);
Assert.NotNull(result.Model);
Assert.True(context.HttpContext.Request.Body.CanRead);
}
@ -335,7 +342,11 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(inputBytes, contentType: "application/xml; charset=utf-16");
var context = new InputFormatterContext(httpContext, modelState, typeof(TestLevelOne));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(TestLevelOne));
// Act and Assert
var ex = await Assert.ThrowsAsync(expectedException, () => formatter.ReadAsync(context));
@ -363,14 +374,15 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
var levelTwoModel = model as TestLevelTwo;
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelTwo>(result.Model);
Buffer.BlockCopy(sampleStringBytes, 0, expectedBytes, 0, sampleStringBytes.Length);
Buffer.BlockCopy(bom, 0, expectedBytes, sampleStringBytes.Length, bom.Length);
Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(levelTwoModel.SampleString));
Assert.Equal(expectedBytes, Encoding.UTF8.GetBytes(model.SampleString));
}
[Fact]
@ -391,25 +403,33 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml
var modelState = new ModelStateDictionary();
var httpContext = GetHttpContext(contentBytes, contentType: "application/xml; charset=utf-16");
var context = new InputFormatterContext(httpContext, modelState, typeof(TestLevelOne));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
modelType: typeof(TestLevelOne));
// Act
var model = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(model);
Assert.IsType<TestLevelOne>(model);
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
var levelOneModel = model as TestLevelOne;
Assert.Equal(expectedInt, levelOneModel.SampleInt);
Assert.Equal(expectedString, levelOneModel.sampleString);
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), levelOneModel.SampleDate);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate);
}
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
{
var httpContext = GetHttpContext(contentBytes);
return new InputFormatterContext(httpContext, new ModelStateDictionary(), modelType);
return new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: new ModelStateDictionary(),
modelType: modelType);
}
private static HttpContext GetHttpContext(

View File

@ -7,7 +7,6 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Actions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Testing;
using Xunit;
namespace Microsoft.AspNet.Mvc.IntegrationTests
@ -111,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
public int Address { get; set; }
}
[Fact(Skip = "#2722 validation error from formatter is recorded with the wrong key.")]
[Fact]
public async Task FromBodyAndRequiredOnValueTypeProperty_EmptyBody_JsonFormatterAddsModelStateError()
{
// Arrange
@ -146,7 +145,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var entry = Assert.Single(modelState);
Assert.Equal("CustomParameter.Address", entry.Key);
Assert.Null(entry.Value.AttemptedValue);
Assert.Same(boundPerson, entry.Value.RawValue);
Assert.Null(entry.Value.RawValue);
var error = Assert.Single(entry.Value.Errors);
Assert.NotNull(error.Exception);
@ -155,6 +154,118 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.NotEmpty(error.Exception.Message);
}
private class Person5
{
[FromBody]
public Address5 Address { get; set; }
}
private class Address5
{
public int Number { get; set; }
// Required attribute does not cause an error in test scenarios. JSON deserializer ok w/ missing data.
[Required]
public int RequiredNumber { get; set; }
}
[Fact]
public async Task FromBodyAndRequiredOnInnerValueTypeProperty_NotBound_JsonFormatterSuccessful()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo
{
BinderModelName = "CustomParameter",
},
ParameterType = typeof(Person5)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request =>
{
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Number\": 5 }"));
request.ContentType = "application/json";
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var boundPerson = Assert.IsType<Person5>(modelBindingResult.Model);
Assert.NotNull(boundPerson.Address);
Assert.Equal(5, boundPerson.Address.Number);
Assert.Equal(0, boundPerson.Address.RequiredNumber);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("CustomParameter.Address", entry.Key);
Assert.NotNull(entry.Value);
Assert.Null(entry.Value.AttemptedValue);
Assert.Same(boundPerson.Address, entry.Value.RawValue);
Assert.Empty(entry.Value.Errors);
}
[Fact]
public async Task FromBodyWithInvalidPropertyData_JsonFormatterAddsModelError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo
{
BinderModelName = "CustomParameter",
},
ParameterType = typeof(Person5)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request =>
{
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Number\": \"not a number\" }"));
request.ContentType = "application/json";
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var boundPerson = Assert.IsType<Person5>(modelBindingResult.Model);
Assert.Null(boundPerson.Address);
Assert.False(modelState.IsValid);
Assert.Equal(2, modelState.Count);
Assert.Equal(1, modelState.ErrorCount);
var state = modelState["CustomParameter.Address"];
Assert.NotNull(state);
Assert.Null(state.AttemptedValue);
Assert.Null(state.RawValue);
Assert.Empty(state.Errors);
state = modelState["CustomParameter.Address.Number"];
Assert.NotNull(state);
Assert.Null(state.AttemptedValue);
Assert.Null(state.RawValue);
var error = Assert.Single(state.Errors);
Assert.NotNull(error.Exception);
// Json.NET currently throws an Exception with a Message starting with "Could not convert string to
// integer: not a number." but do not tie test to a particular Json.NET build.
Assert.NotEmpty(error.Exception.Message);
}
private class Person2
{
[FromBody]

View File

@ -4,6 +4,7 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
@ -19,17 +20,19 @@ namespace FormatterWebSite
SupportedEncodings.Add(Encoding.Unicode);
}
public override Task<object> ReadRequestBodyAsync(InputFormatterContext context)
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
var effectiveEncoding = SelectCharacterEncoding(context);
if (effectiveEncoding == null)
{
return InputFormatterResult.FailureAsync();
}
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body, effectiveEncoding))
{
var stringContent = reader.ReadToEnd();
return Task.FromResult<object>(stringContent);
return InputFormatterResult.SuccessAsync(stringContent);
}
}
}