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

View File

@ -8,6 +8,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public class ModelBindingResult 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> /// <summary>
/// Creates a new <see cref="ModelBindingResult"/>. /// Creates a new <see cref="ModelBindingResult"/>.
/// </summary> /// </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 /// <param name="isModelSet">A value that represents if the model has been set by the
/// <see cref="IModelBinder"/>.</param> /// <see cref="IModelBinder"/>.</param>
public ModelBindingResult(object model, string key, bool isModelSet) 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> /// </summary>
public string Key { get; } 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> /// <summary>
/// <para> /// <para>
/// Gets a value indicating whether or not the <see cref="Model"/> value has been set. /// 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 result = await modelBinder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ? var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) : result :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// A model binder was specified by metadata and this binder handles all such cases. // 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 result = await BindModelCoreAsync(context);
var modelBindingResult = var modelBindingResult = result != null ?
result != null ? result :
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) : new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
// This model binder is the only handler for its binding source. // 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. // 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Core;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
@ -28,36 +27,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected async override Task<ModelBindingResult> BindModelCoreAsync( protected async override Task<ModelBindingResult> BindModelCoreAsync(
[NotNull] ModelBindingContext bindingContext) [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 httpContext = bindingContext.OperationBindingContext.HttpContext;
var formatters = bindingContext.OperationBindingContext.InputFormatters;
var formatterContext = new InputFormatterContext( var formatterContext = new InputFormatterContext(
httpContext, httpContext,
bindingContext.ModelState, bindingContext.ModelState,
bindingContext.ModelType); bindingContext.ModelType);
var formatters = bindingContext.OperationBindingContext.InputFormatters;
var formatter = formatters.FirstOrDefault(f => f.CanRead(formatterContext)); var formatter = formatters.FirstOrDefault(f => f.CanRead(formatterContext));
if (formatter == null) if (formatter == null)
{ {
var unsupportedContentType = Resources.FormatUnsupportedContentType( var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType); 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. // This model binder is the only handler for the Body binding source and it cannot run twice. Always
// Always tell the model binding system to skip other model binders i.e. return non-null. // tell the model binding system to skip other model binders and never to fall back i.e. indicate a
return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); // fatal error.
return new ModelBindingResult(modelBindingKey);
} }
try try
{ {
var previousCount = bindingContext.ModelState.ErrorCount;
var model = await formatter.ReadAsync(formatterContext); 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) var validationNode = new ModelValidationNode(modelBindingKey, bindingContext.ModelMetadata, model)
{ {
@ -72,11 +80,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
catch (Exception ex) 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. // This model binder is the only handler for the Body binding source and it cannot run twice. Always
// Always tell the model binding system to skip other model binders i.e. return non-null. // tell the model binding system to skip other model binders and never to fall back i.e. indicate a
return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); // fatal error.
return new ModelBindingResult(modelBindingKey);
} }
} }
} }

View File

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

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ? var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) : result :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false); new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// Were able to resolve a binder type. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
#if DNXCORE50
using System.Reflection; using System.Reflection;
#endif
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
@ -39,8 +41,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
model = value; model = value;
} }
} }
else if (typeof(IEnumerable<string>).GetTypeInfo().IsAssignableFrom( else if (typeof(IEnumerable<string>).IsAssignableFrom(bindingContext.ModelType))
bindingContext.ModelType.GetTypeInfo()))
{ {
var values = request.Headers.GetCommaSeparatedValues(headerName); var values = request.Headers.GetCommaSeparatedValues(headerName);
if (values != null) if (values != null)

View File

@ -1962,38 +1962,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ModelTypeIsWrong"), p0, 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> /// <summary>
/// A value for the '{0}' property was not provided. /// A value for the '{0}' property was not provided.
/// </summary> /// </summary>

View File

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

View File

@ -1898,102 +1898,6 @@ namespace Microsoft.AspNet.Mvc.Extensions
return GetString("KeyValuePair_BothKeyAndValueMustBePresent"); 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> /// <summary>
/// The '{0}' property is required. /// The '{0}' property is required.
/// </summary> /// </summary>

View File

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

View File

@ -77,10 +77,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type. // Returns true because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); 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] [Fact]
@ -99,6 +104,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
} }
@ -167,11 +173,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type. // Returns true because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); 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); Assert.Equal("Your input is bad!", errorMessage);
} }
@ -198,13 +208,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
// Returns true because it understands the metadata type. // Returns non-null result because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
Assert.Null(binderResult.ValidationNode); 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); Assert.Equal("Unsupported content type 'text/xyz'.", errorMessage);
} }

View File

@ -65,7 +65,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act // Act
var response = await client.PostAsync("http://localhost/Validation/Index", content); var response = await client.PostAsync("http://localhost/Validation/Index", content);
//Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("User has been registerd : " + sampleName, Assert.Equal("User has been registerd : " + sampleName,
await response.Content.ReadAsStringAsync()); await response.Content.ReadAsStringAsync());
@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act // Act
var response = await client.PostAsync("http://localhost/Validation/Index", content); var response = await client.PostAsync("http://localhost/Validation/Index", content);
//Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("The field Id must be between 1 and 2000.," + 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'.," + "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 // Act
var response = await client.PostAsync("http://localhost/Validation/GetDeveloperName", content); var response = await client.PostAsync("http://localhost/Validation/GetDeveloperName", content);
//Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); 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()); await response.Content.ReadAsStringAsync());
} }
@ -123,33 +123,33 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient(); var client = server.CreateClient();
var requestData = "{\"Name\":\"Library Manager\", \"Suppliers\": [{\"Name\":\"Contoso Corp\"}]}"; var requestData = "{\"Name\":\"Library Manager\", \"Suppliers\": [{\"Name\":\"Contoso Corp\"}]}";
var content = new StringContent(requestData, Encoding.UTF8, "application/json"); 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'."; = "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'."; = "The field Name must be a string or array type with a maximum length of '5'.";
// Act // Act
var response = await client.PostAsync("http://localhost/Validation/CreateProject", content); var response = await client.PostAsync("http://localhost/Validation/CreateProject", content);
//Assert // Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent); var responseObject = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any()); var errorKeyValuePair = Assert.Single(responseObject, keyValuePair => keyValuePair.Value.Length > 0);
var error = Assert.Single(errorCollection.Value.Errors); var errorMessage = Assert.Single(errorKeyValuePair.Value);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); Assert.Equal(expectedModelStateErrorMessage, errorMessage);
// verifies that the excluded type is not validated // verifies that the excluded type is not validated
Assert.NotEqual(shouldNotContainMessage, error.ErrorMessage); Assert.NotEqual(shouldNotContainMessage, errorMessage);
} }
[Theory] [Theory]
[MemberData(nameof(SimpleTypePropertiesModelRequestData))] [MemberData(nameof(SimpleTypePropertiesModelRequestData))]
public async Task ShallowValidation_HappensOnExlcuded_SimpleTypeProperties( public async Task ShallowValidation_HappensOnExcluded_SimpleTypeProperties(
string requestContent, string requestContent,
int expectedStatusCode, int expectedStatusCode,
string expectedModelStateErrorMessage) string expectedModelStateErrorMessage)
{ {
// Arrange // Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices); 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"); var content = new StringContent(requestContent, Encoding.UTF8, "application/json");
// Act // Act
var response = await client.PostAsync("http://localhost/Validation/CreateSimpleTypePropertiesModel", var response = await client.PostAsync(
content); "http://localhost/Validation/CreateSimpleTypePropertiesModel",
content);
//Assert // Assert
Assert.Equal(expectedStatusCode, (int)response.StatusCode); Assert.Equal(expectedStatusCode, (int)response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent); var responseObject = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(responseContent);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any()); var errorKeyValuePair = Assert.Single(responseObject, keyValuePair => keyValuePair.Value.Length > 0);
var error = Assert.Single(errorCollection.Value.Errors); var errorMessage = Assert.Single(errorKeyValuePair.Value);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); Assert.Equal(expectedModelStateErrorMessage, errorMessage);
} }
[Fact] [Fact]
@ -183,27 +184,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act // Act
var response = await client.PostAsync("http://localhost/Validation/GetDeveloperAlias", content); var response = await client.PostAsync("http://localhost/Validation/GetDeveloperAlias", content);
//Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("xyz", await response.Content.ReadAsStringAsync()); 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.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using ModelBindingWebSite.Models; using ModelBindingWebSite.Models;
using ModelBindingWebSite.ViewModels; using ModelBindingWebSite.ViewModels;
@ -39,9 +38,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var response = await client.GetStringAsync("http://localhost/Validation/DoNotValidateParameter"); var response = await client.GetStringAsync("http://localhost/Validation/DoNotValidateParameter");
// Assert // Assert
var modelState = JsonConvert.DeserializeObject<ModelStateDictionary>(response); Assert.Equal("true", response);
Assert.Empty(modelState);
Assert.True(modelState.IsValid);
} }
[Fact] [Fact]
@ -52,13 +49,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient(); var client = server.CreateClient();
// Act // Act
var response = await client.GetAsync( var response = await client.GetStringAsync("http://localhost/Validation/AvoidRecursive?Name=selfish");
"http://localhost/Validation/AvoidRecursive?Name=selfish");
// Assert // Assert
var stringValue = await response.Content.ReadAsStringAsync(); Assert.Equal("true", response);
var json = JsonConvert.DeserializeObject<ModelStateDictionary>(stringValue);
Assert.True(json.IsValid);
} }
[Theory] [Theory]

View File

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

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert // Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync(); 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) if (!ActionContext.ModelState.IsValid)
{ {
var parameterBindingErrors = ActionContext.ModelState["dummy"].Errors; // Body model binder normally reports errors for parameters using the empty name.
if (parameterBindingErrors.Count != 0) var parameterBindingErrors = ActionContext.ModelState["dummy"]?.Errors ??
ActionContext.ModelState[string.Empty]?.Errors;
if (parameterBindingErrors != null && parameterBindingErrors.Count != 0)
{ {
return new ErrorInfo 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
namespace FormatterWebSite 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 // property Developers should happen
[ModelStateValidationFilter] [ModelStateValidationFilter]
public IActionResult CreateProject([FromBody]Project project) public IActionResult CreateProject([FromBody] Project project)
{ {
return Json(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. // 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.Mvc;
using Microsoft.AspNet.WebUtilities;
namespace FormatterWebSite namespace FormatterWebSite
{ {
@ -12,10 +11,7 @@ namespace FormatterWebSite
{ {
if (!context.ModelState.IsValid) if (!context.ModelState.IsValid)
{ {
context.Result = new ObjectResult(context.ModelState) context.Result = new BadRequestObjectResult(context.ModelState);
{
StatusCode = StatusCodes.Status400BadRequest
};
} }
} }
} }

View File

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

View File

@ -1,10 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using Microsoft.AspNet.Mvc.ModelBinding;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite.Controllers namespace ModelBindingWebSite.Controllers
{ {
@ -16,61 +13,19 @@ namespace ModelBindingWebSite.Controllers
public object AvoidRecursive(SelfishPerson selfishPerson) public object AvoidRecursive(SelfishPerson selfishPerson)
{ {
return new SerializableModelStateDictionary(ModelState); return ModelState.IsValid;
} }
public object DoNotValidateParameter([FromServices] ITestService service) 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 class SelfishPerson
{ {
public string Name { get; set; } public string Name { get; set; }
public SelfishPerson MySelf { get { return this; } } public SelfishPerson MySelf { get { return this; } }
} }
} }