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;
} }
@ -68,7 +72,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
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,
@ -168,8 +172,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <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

@ -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,7 +109,7 @@ 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());
@ -131,25 +131,25 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// 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,7 +49,7 @@ 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
@ -59,7 +57,7 @@ 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; } }
} }
} }