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;
}
@ -68,7 +72,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var binderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName;
var propertyPredicateProvider =
bindingInfo?.PropertyBindingPredicateProvider ?? metadata.PropertyBindingPredicateProvider;
return new ModelBindingContext()
return new ModelBindingContext
{
ModelMetadata = metadata,
BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource,
@ -168,8 +172,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <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,
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

@ -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,7 +109,7 @@ 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.",
await response.Content.ReadAsStringAsync());
@ -131,25 +131,25 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// 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,7 +49,7 @@ 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}",
":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
@ -59,7 +57,7 @@ namespace FormatterWebSite
// '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; } }
}
}