Add `ModelBindingResult.IsFatalError` and make body binding more consistent

- part I of II for #2445 (with a duplicate code PR to follow)
- needed for #2445 because new `ModelState` entries for values will make inconsisteny worse
- change `BodyModelBinder` to use same keys for all `ModelBindingResult`s and `ModelState` entries
 - return fatal error result if formatter adds an error to `ModelState`
 - update potential callers to avoid avoid ignoring `IsFatalError`
- fix test attempting to serialize all of `ModelState`
 - will be borked with additional `RawValue`s in state
- fix two other tests that serialized `ModelState` but checked only `IsValid`

nits:
- address minor inconsistencies in `ModelBindingContext`
- use `System.Reflection.Extensions` package a bit more, where it's already referenced
- remove some unused resources
This commit is contained in:
Doug Bunting 2015-06-13 18:56:50 -07:00
parent b245996949
commit c4fa402105
22 changed files with 178 additions and 352 deletions

View File

@ -39,16 +39,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[NotNull] string modelName,
[NotNull] ModelMetadata modelMetadata)
{
var modelBindingContext = new ModelBindingContext();
modelBindingContext.ModelName = modelName;
modelBindingContext.ModelMetadata = modelMetadata;
modelBindingContext.ModelState = bindingContext.ModelState;
modelBindingContext.ValueProvider = bindingContext.ValueProvider;
modelBindingContext.OperationBindingContext = bindingContext.OperationBindingContext;
var modelBindingContext = new ModelBindingContext
{
ModelName = modelName,
ModelMetadata = modelMetadata,
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider,
OperationBindingContext = bindingContext.OperationBindingContext,
BindingSource = modelMetadata.BindingSource,
BinderModelName = modelMetadata.BinderModelName,
BinderType = modelMetadata.BinderType,
};
modelBindingContext.BindingSource = modelMetadata.BindingSource;
modelBindingContext.BinderModelName = modelMetadata.BinderModelName;
modelBindingContext.BinderType = modelMetadata.BinderType;
return modelBindingContext;
}
@ -66,9 +70,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
string modelName)
{
var binderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName;
var propertyPredicateProvider =
var propertyPredicateProvider =
bindingInfo?.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider;
return new ModelBindingContext()
return new ModelBindingContext
{
ModelMetadata = metadata,
BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource,
@ -149,27 +153,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <summary>
/// Gets or sets a model name which is explicitly set using an <see cref="IModelNameProvider"/>.
/// Gets or sets a model name which is explicitly set using an <see cref="IModelNameProvider"/>.
/// <see cref="Model"/>.
/// </summary>
public string BinderModelName { get; set; }
/// <summary>
/// Gets or sets a value which represents the <see cref="BindingSource"/> associated with the
/// Gets or sets a value which represents the <see cref="BindingSource"/> associated with the
/// <see cref="Model"/>.
/// </summary>
public BindingSource BindingSource { get; set; }
/// <summary>
/// Gets the <see cref="Type"/> of an <see cref="IModelBinder"/> associated with the
/// Gets the <see cref="Type"/> of an <see cref="IModelBinder"/> associated with the
/// <see cref="Model"/>.
/// </summary>
public Type BinderType { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether the binder should use an empty prefix to look up
/// values in <see cref="IValueProvider"/> when no values are found using the
/// <see cref="ModelName"/> prefix.
/// values in <see cref="IValueProvider"/> when no values are found using the <see cref="ModelName"/> prefix.
/// </summary>
public bool FallbackToEmptyPrefix { get; set; }

View File

@ -8,6 +8,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public class ModelBindingResult
{
/// <summary>
/// Creates a new <see cref="ModelBindingResult"/> indicating a fatal error.
/// </summary>
/// <param name="key">The key using which was used to attempt binding the model.</param>
public ModelBindingResult(string key)
{
Key = key;
IsFatalError = true;
}
/// <summary>
/// Creates a new <see cref="ModelBindingResult"/>.
/// </summary>
@ -16,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <param name="isModelSet">A value that represents if the model has been set by the
/// <see cref="IModelBinder"/>.</param>
public ModelBindingResult(object model, string key, bool isModelSet)
: this (model, key, isModelSet, validationNode: null)
: this(model, key, isModelSet, validationNode: null)
{
}
@ -52,6 +62,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public string Key { get; }
/// <summary>
/// Gets a value indicating the caller should not attempt binding again. This attempt encountered a fatal
/// error.
/// </summary>
public bool IsFatalError { get; }
/// <summary>
/// <para>
/// Gets a value indicating whether or not the <see cref="Model"/> value has been set.

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await modelBinder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
result :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// A model binder was specified by metadata and this binder handles all such cases.

View File

@ -79,10 +79,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await BindModelCoreAsync(context);
var modelBindingResult =
result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
var modelBindingResult = result != null ?
result :
new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
// This model binder is the only handler for its binding source.
// Always tell the model binding system to skip other model binders i.e. return non-null.

View File

@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -28,36 +27,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected async override Task<ModelBindingResult> BindModelCoreAsync(
[NotNull] ModelBindingContext bindingContext)
{
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// the parameter name/a custom name. In all other cases (like when binding body to a property) we
// consider the entire ModelName as a prefix.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName;
var httpContext = bindingContext.OperationBindingContext.HttpContext;
var formatters = bindingContext.OperationBindingContext.InputFormatters;
var formatterContext = new InputFormatterContext(
httpContext,
bindingContext.ModelState,
httpContext,
bindingContext.ModelState,
bindingContext.ModelType);
var formatters = bindingContext.OperationBindingContext.InputFormatters;
var formatter = formatters.FirstOrDefault(f => f.CanRead(formatterContext));
if (formatter == null)
{
var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType);
bindingContext.ModelState.AddModelError(modelBindingKey, unsupportedContentType);
// This model binder is the only handler for the Body binding source.
// Always tell the model binding system to skip other model binders i.e. return non-null.
return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// This model binder is the only handler for the Body binding source and it cannot run twice. Always
// tell the model binding system to skip other model binders and never to fall back i.e. indicate a
// fatal error.
return new ModelBindingResult(modelBindingKey);
}
try
{
var previousCount = bindingContext.ModelState.ErrorCount;
var model = await formatter.ReadAsync(formatterContext);
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
if (bindingContext.ModelState.ErrorCount != previousCount)
{
// 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.
return new ModelBindingResult(modelBindingKey);
}
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// the parameter name/a custom name. In all other cases (like when binding body to a property) we
// consider the entire ModelName as a prefix.
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName;
var validationNode = new ModelValidationNode(modelBindingKey, bindingContext.ModelMetadata, model)
{
@ -72,11 +80,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
bindingContext.ModelState.AddModelError(modelBindingKey, ex);
// This model binder is the only handler for the Body binding source.
// Always tell the model binding system to skip other model binders i.e. return non-null.
return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// This model binder is the only handler for the Body binding source and it cannot run twice. Always
// tell the model binding system to skip other model binders and never to fall back i.e. indicate a
// fatal error.
return new ModelBindingResult(modelBindingKey);
}
}
}

View File

@ -42,8 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
!string.IsNullOrEmpty(bindingContext.ModelName))
{
// Fall back to empty prefix.
newBindingContext = CreateNewBindingContext(bindingContext,
modelName: string.Empty);
newBindingContext = CreateNewBindingContext(bindingContext, modelName: string.Empty);
modelBindingResult = await TryBind(newBindingContext);
}
@ -99,13 +98,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
if (result != null)
{
// Use returned ModelBindingResult if it either indicates the model was set or is related to a
// ModelState entry. The second condition is necessary because the ModelState entry would never be
// validated if caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid.
// Use returned ModelBindingResult if it indicates the model was set, indicates the binder
// encountered a fatal error, or is related to a ModelState entry.
//
// In most (hopefully all) cases, the ModelState entry exists because some binders add errors
// before returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsModelSet || bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
// The second condition is necessary because the BodyModelBinder unconditionally binds during the
// first attempt and does not always create ModelState values using ModelName.
//
// The third condition is necessary because the ModelState entry would never be validated if
// caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid. In most
// (hopefully all) cases, the ModelState entry exists because some binders add errors before
// returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsFatalError ||
result.IsModelSet ||
bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
{
return result;
}
@ -121,8 +126,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return null;
}
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName)
private static ModelBindingContext CreateNewBindingContext(
ModelBindingContext oldBindingContext,
string modelName)
{
var newBindingContext = new ModelBindingContext
{

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
result :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// Were able to resolve a binder type.

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
#if DNXCORE50
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
@ -39,8 +41,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
model = value;
}
}
else if (typeof(IEnumerable<string>).GetTypeInfo().IsAssignableFrom(
bindingContext.ModelType.GetTypeInfo()))
else if (typeof(IEnumerable<string>).IsAssignableFrom(bindingContext.ModelType))
{
var values = request.Headers.GetCommaSeparatedValues(headerName);
if (values != null)

View File

@ -1962,38 +1962,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ModelTypeIsWrong"), p0, p1);
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string ModelBinderUtil_ValueInvalid
{
get { return GetString("ModelBinderUtil_ValueInvalid"); }
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string FormatModelBinderUtil_ValueInvalid(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalid"), p0, p1);
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string ModelBinderUtil_ValueInvalidGeneric
{
get { return GetString("ModelBinderUtil_ValueInvalidGeneric"); }
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string FormatModelBinderUtil_ValueInvalidGeneric(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalidGeneric"), p0);
}
/// <summary>
/// A value for the '{0}' property was not provided.
/// </summary>

View File

@ -493,12 +493,6 @@
<data name="ModelBinderUtil_ModelTypeIsWrong" xml:space="preserve">
<value>The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.</value>
</data>
<data name="ModelBinderUtil_ValueInvalid" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value>
</data>
<data name="ModelBinderUtil_ValueInvalidGeneric" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value>
</data>
<data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve">
<value>A value for the '{0}' property was not provided.</value>
</data>

View File

@ -1898,102 +1898,6 @@ namespace Microsoft.AspNet.Mvc.Extensions
return GetString("KeyValuePair_BothKeyAndValueMustBePresent");
}
/// <summary>
/// The binding context has a null Model, but this binder requires a non-null model of type '{0}'.
/// </summary>
internal static string ModelBinderUtil_ModelCannotBeNull
{
get { return GetString("ModelBinderUtil_ModelCannotBeNull"); }
}
/// <summary>
/// The binding context has a null Model, but this binder requires a non-null model of type '{0}'.
/// </summary>
internal static string FormatModelBinderUtil_ModelCannotBeNull(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ModelCannotBeNull"), p0);
}
/// <summary>
/// The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.
/// </summary>
internal static string ModelBinderUtil_ModelInstanceIsWrong
{
get { return GetString("ModelBinderUtil_ModelInstanceIsWrong"); }
}
/// <summary>
/// The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.
/// </summary>
internal static string FormatModelBinderUtil_ModelInstanceIsWrong(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ModelInstanceIsWrong"), p0, p1);
}
/// <summary>
/// The binding context cannot have a null ModelMetadata.
/// </summary>
internal static string ModelBinderUtil_ModelMetadataCannotBeNull
{
get { return GetString("ModelBinderUtil_ModelMetadataCannotBeNull"); }
}
/// <summary>
/// The binding context cannot have a null ModelMetadata.
/// </summary>
internal static string FormatModelBinderUtil_ModelMetadataCannotBeNull()
{
return GetString("ModelBinderUtil_ModelMetadataCannotBeNull");
}
/// <summary>
/// The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.
/// </summary>
internal static string ModelBinderUtil_ModelTypeIsWrong
{
get { return GetString("ModelBinderUtil_ModelTypeIsWrong"); }
}
/// <summary>
/// The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.
/// </summary>
internal static string FormatModelBinderUtil_ModelTypeIsWrong(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ModelTypeIsWrong"), p0, p1);
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string ModelBinderUtil_ValueInvalid
{
get { return GetString("ModelBinderUtil_ValueInvalid"); }
}
/// <summary>
/// The value '{0}' is not valid for {1}.
/// </summary>
internal static string FormatModelBinderUtil_ValueInvalid(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalid"), p0, p1);
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string ModelBinderUtil_ValueInvalidGeneric
{
get { return GetString("ModelBinderUtil_ValueInvalidGeneric"); }
}
/// <summary>
/// The supplied value is invalid for {0}.
/// </summary>
internal static string FormatModelBinderUtil_ValueInvalidGeneric(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalidGeneric"), p0);
}
/// <summary>
/// The '{0}' property is required.
/// </summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -481,24 +481,6 @@
<data name="KeyValuePair_BothKeyAndValueMustBePresent" xml:space="preserve">
<value>A value is required.</value>
</data>
<data name="ModelBinderUtil_ModelCannotBeNull" xml:space="preserve">
<value>The binding context has a null Model, but this binder requires a non-null model of type '{0}'.</value>
</data>
<data name="ModelBinderUtil_ModelInstanceIsWrong" xml:space="preserve">
<value>The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.</value>
</data>
<data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve">
<value>The binding context cannot have a null ModelMetadata.</value>
</data>
<data name="ModelBinderUtil_ModelTypeIsWrong" xml:space="preserve">
<value>The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.</value>
</data>
<data name="ModelBinderUtil_ValueInvalid" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value>
</data>
<data name="ModelBinderUtil_ValueInvalidGeneric" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value>
</data>
<data name="ModelBinding_MissingRequiredMember" xml:space="preserve">
<value>The '{0}' property is required.</value>
</data>

View File

@ -77,10 +77,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type.
Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
// Key is empty because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
Assert.Single(entry.Value.Errors);
}
[Fact]
@ -99,6 +104,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert
Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
}
@ -167,11 +173,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type.
Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].Exception.Message;
// Key is empty because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("Your input is bad!", errorMessage);
}
@ -198,13 +208,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert
// Returns true because it understands the metadata type.
// Returns non-null result because it understands the metadata type.
Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model);
Assert.Null(binderResult.ValidationNode);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].ErrorMessage;
// Key is empty because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("Unsupported content type 'text/xyz'.", errorMessage);
}

View File

@ -65,7 +65,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.PostAsync("http://localhost/Validation/Index", content);
//Assert
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("User has been registerd : " + sampleName,
await response.Content.ReadAsStringAsync());
@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.PostAsync("http://localhost/Validation/Index", content);
//Assert
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("The field Id must be between 1 and 2000.," +
"The field Name must be a string or array type with a minimum length of '5'.," +
@ -109,9 +109,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.PostAsync("http://localhost/Validation/GetDeveloperName", content);
//Assert
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("No model validation for developer, even though developer.Name is empty.",
Assert.Equal("No model validation for developer, even though developer.Name is empty.",
await response.Content.ReadAsStringAsync());
}
@ -123,33 +123,33 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
var requestData = "{\"Name\":\"Library Manager\", \"Suppliers\": [{\"Name\":\"Contoso Corp\"}]}";
var content = new StringContent(requestData, Encoding.UTF8, "application/json");
var expectedModelStateErrorMessage
var expectedModelStateErrorMessage
= "The field Suppliers must be a string or array type with a minimum length of '2'.";
var shouldNotContainMessage
var shouldNotContainMessage
= "The field Name must be a string or array type with a maximum length of '5'.";
// Act
var response = await client.PostAsync("http://localhost/Validation/CreateProject", content);
//Assert
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any());
var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage);
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
var errorKeyValuePair = Assert.Single(responseObject, keyValuePair => keyValuePair.Value.Length > 0);
var errorMessage = Assert.Single(errorKeyValuePair.Value);
Assert.Equal(expectedModelStateErrorMessage, errorMessage);
// verifies that the excluded type is not validated
Assert.NotEqual(shouldNotContainMessage, error.ErrorMessage);
Assert.NotEqual(shouldNotContainMessage, errorMessage);
}
[Theory]
[MemberData(nameof(SimpleTypePropertiesModelRequestData))]
public async Task ShallowValidation_HappensOnExlcuded_SimpleTypeProperties(
string requestContent,
int expectedStatusCode,
string expectedModelStateErrorMessage)
public async Task ShallowValidation_HappensOnExcluded_SimpleTypeProperties(
string requestContent,
int expectedStatusCode,
string expectedModelStateErrorMessage)
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -157,17 +157,18 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var content = new StringContent(requestContent, Encoding.UTF8, "application/json");
// Act
var response = await client.PostAsync("http://localhost/Validation/CreateSimpleTypePropertiesModel",
content);
var response = await client.PostAsync(
"http://localhost/Validation/CreateSimpleTypePropertiesModel",
content);
//Assert
// Assert
Assert.Equal(expectedStatusCode, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any());
var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage);
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
var errorKeyValuePair = Assert.Single(responseObject, keyValuePair => keyValuePair.Value.Length > 0);
var errorMessage = Assert.Single(errorKeyValuePair.Value);
Assert.Equal(expectedModelStateErrorMessage, errorMessage);
}
[Fact]
@ -183,27 +184,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.PostAsync("http://localhost/Validation/GetDeveloperAlias", content);
//Assert
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("xyz", await response.Content.ReadAsStringAsync());
}
private class ErrorCollection
{
public IEnumerable<Error> Errors
{
get;
set;
}
}
private class Error
{
public string ErrorMessage
{
get;
set;
}
}
}
}

View File

@ -11,7 +11,6 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using ModelBindingWebSite.Models;
using ModelBindingWebSite.ViewModels;
@ -39,9 +38,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var response = await client.GetStringAsync("http://localhost/Validation/DoNotValidateParameter");
// Assert
var modelState = JsonConvert.DeserializeObject<ModelStateDictionary>(response);
Assert.Empty(modelState);
Assert.True(modelState.IsValid);
Assert.Equal("true", response);
}
[Fact]
@ -52,13 +49,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/Validation/AvoidRecursive?Name=selfish");
var response = await client.GetStringAsync("http://localhost/Validation/AvoidRecursive?Name=selfish");
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var json = JsonConvert.DeserializeObject<ModelStateDictionary>(stringValue);
Assert.True(json.IsValid);
Assert.Equal("true", response);
}
[Theory]

View File

@ -49,8 +49,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var data = await response.Content.ReadAsStringAsync();
Assert.Contains(
string.Format(
"dummyObject:There was an error deserializing the object of type {0}",
typeof(DummyClass).FullName),
":There was an error deserializing the object of type {0}.",
typeof(DummyClass).FullName),
data);
}

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync();
Assert.Contains("dummyObject:There is an error in XML document", data);
Assert.Contains(":There is an error in XML document", data);
}
}
}

View File

@ -14,8 +14,10 @@ namespace FormatterWebSite.Controllers
{
if (!ActionContext.ModelState.IsValid)
{
var parameterBindingErrors = ActionContext.ModelState["dummy"].Errors;
if (parameterBindingErrors.Count != 0)
// Body model binder normally reports errors for parameters using the empty name.
var parameterBindingErrors = ActionContext.ModelState["dummy"]?.Errors ??
ActionContext.ModelState[string.Empty]?.Errors;
if (parameterBindingErrors != null && parameterBindingErrors.Count != 0)
{
return new ErrorInfo
{

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace FormatterWebSite
@ -56,10 +54,10 @@ namespace FormatterWebSite
}
}
// 'Developer' type is excluded but the shallow validation on the
// 'Developer' type is excluded but the shallow validation on the
// property Developers should happen
[ModelStateValidationFilter]
public IActionResult CreateProject([FromBody]Project project)
public IActionResult CreateProject([FromBody] Project project)
{
return Json(project);
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.WebUtilities;
namespace FormatterWebSite
{
@ -12,10 +11,7 @@ namespace FormatterWebSite
{
if (!context.ModelState.IsValid)
{
context.Result = new ObjectResult(context.ModelState)
{
StatusCode = StatusCodes.Status400BadRequest
};
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}

View File

@ -19,8 +19,10 @@ namespace FormatterWebSite
parameter.BindingInfo?.BindingSource));
if (bodyParameter != null)
{
var parameterBindingErrors = context.ModelState[bodyParameter.Name].Errors;
if (parameterBindingErrors.Count != 0)
// Body model binder normally reports errors for parameters using the empty name.
var parameterBindingErrors = context.ModelState[bodyParameter.Name]?.Errors ??
context.ModelState[string.Empty]?.Errors;
if (parameterBindingErrors != null && parameterBindingErrors.Count != 0)
{
var errorInfo = new ErrorInfo
{

View File

@ -1,10 +1,7 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite.Controllers
{
@ -16,61 +13,19 @@ namespace ModelBindingWebSite.Controllers
public object AvoidRecursive(SelfishPerson selfishPerson)
{
return new SerializableModelStateDictionary(ModelState);
return ModelState.IsValid;
}
public object DoNotValidateParameter([FromServices] ITestService service)
{
return ModelState;
return ModelState.IsValid;
}
}
public class SerializableModelStateDictionary : Dictionary<string, Entry>
{
public bool IsValid { get; set; }
public int ErrorCount { get; set; }
public SerializableModelStateDictionary(ModelStateDictionary modelState)
{
var errorCount = 0;
foreach (var keyModelStatePair in modelState)
{
var key = keyModelStatePair.Key;
var value = keyModelStatePair.Value;
errorCount += value.Errors.Count;
var entry = new Entry()
{
Errors = value.Errors,
RawValue = value.Value.RawValue,
AttemptedValue = value.Value.AttemptedValue,
ValidationState = value.ValidationState
};
Add(key, entry);
}
IsValid = modelState.IsValid;
ErrorCount = errorCount;
}
}
public class Entry
{
public ModelValidationState ValidationState { get; set; }
public ModelErrorCollection Errors { get; set; }
public object RawValue { get; set; }
public string AttemptedValue { get; set; }
}
public class SelfishPerson
{
public string Name { get; set; }
public SelfishPerson MySelf { get { return this; } }
}
}