Fixes #2464 - Does not add extra skipped entries for model bound from services.

Also ensures that when a type is marked as skipped, any sub property which is model bound (and hence a modelstate un validated entry),
is marked as skipped (otherwise it would cause the ModelState to be invalid).
Also fixing a bug in model state dictionary FindKeyWithPrefix was not considering [0] & [0][0] as a valid prefix.
This commit is contained in:
Harsh Gupta 2015-05-13 09:54:20 -07:00
parent 88ac4b94e4
commit d0927bdc75
15 changed files with 354 additions and 140 deletions

17
Mvc.sln
View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.22810.0 VisualStudioVersion = 14.0.22808.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject EndProject
@ -166,6 +166,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExp
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExplorer.Test", "test\Microsoft.AspNet.Mvc.ApiExplorer.Test\Microsoft.AspNet.Mvc.ApiExplorer.Test.xproj", "{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExplorer.Test", "test\Microsoft.AspNet.Mvc.ApiExplorer.Test\Microsoft.AspNet.Mvc.ApiExplorer.Test.xproj", "{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}"
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Abstractions.Test", "test\Microsoft.AspNet.Mvc.Abstractions.Test\Microsoft.AspNet.Mvc.Abstractions.Test.xproj", "{DA000953-7532-4DF5-8DB9-8143DF98D999}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -996,6 +998,18 @@ Global
{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.ActiveCfg = Release|Any CPU {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.ActiveCfg = Release|Any CPU
{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.Build.0 = Release|Any CPU {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.Build.0 = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.ActiveCfg = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.Build.0 = Debug|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.Build.0 = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.ActiveCfg = Release|Any CPU
{DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1077,5 +1091,6 @@ Global
{1154203C-7579-4525-906E-BC55268421C1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {1154203C-7579-4525-906E-BC55268421C1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{A2B72833-5D70-4C42-AE85-E0319926FB8A} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {A2B72833-5D70-4C42-AE85-E0319926FB8A} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{DA000953-7532-4DF5-8DB9-8143DF98D999} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -281,7 +281,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns> /// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns>
public ModelValidationState GetFieldValidationState([NotNull] string key) public ModelValidationState GetFieldValidationState([NotNull] string key)
{ {
var entries = FindKeysWithPrefix(this, key); var entries = FindKeysWithPrefix(key);
if (!entries.Any()) if (!entries.Any())
{ {
return ModelValidationState.Unvalidated; return ModelValidationState.Unvalidated;
@ -378,7 +378,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// If key is null or empty, clear all entries in the dictionary // If key is null or empty, clear all entries in the dictionary
// else just clear the ones that have key as prefix // else just clear the ones that have key as prefix
var entries = (string.IsNullOrEmpty(key)) ? var entries = (string.IsNullOrEmpty(key)) ?
_innerDictionary : FindKeysWithPrefix(this, key); _innerDictionary : FindKeysWithPrefix(key);
foreach (var entry in entries) foreach (var entry in entries)
{ {
@ -502,17 +502,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetEnumerator(); return GetEnumerator();
} }
private static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>( public IEnumerable<KeyValuePair<string, ModelState>> FindKeysWithPrefix([NotNull] string prefix)
[NotNull] IDictionary<string, TValue> dictionary,
[NotNull] string prefix)
{ {
TValue exactMatchValue; ModelState exactMatchValue;
if (dictionary.TryGetValue(prefix, out exactMatchValue)) if (_innerDictionary.TryGetValue(prefix, out exactMatchValue))
{ {
yield return new KeyValuePair<string, TValue>(prefix, exactMatchValue); yield return new KeyValuePair<string, ModelState>(prefix, exactMatchValue);
} }
foreach (var entry in dictionary) foreach (var entry in _innerDictionary)
{ {
var key = entry.Key; var key = entry.Key;
@ -521,19 +519,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
continue; continue;
} }
if (key.StartsWith("[", StringComparison.OrdinalIgnoreCase))
{
key = key.Substring(key.IndexOf('.') + 1);
if (string.Equals(prefix, key, StringComparison.Ordinal))
{
yield return entry;
continue;
}
}
if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{ {
continue;
if (key.StartsWith("[", StringComparison.OrdinalIgnoreCase))
{
var subKey = key.Substring(key.IndexOf('.') + 1);
if (!subKey.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (string.Equals(prefix, subKey, StringComparison.Ordinal))
{
yield return entry;
continue;
}
key = subKey;
}
else
{
continue;
}
} }
// Everything is prefixed by the empty string // Everything is prefixed by the empty string

View File

@ -66,12 +66,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var currentValidationNode = validationContext.ValidationNode; var currentValidationNode = validationContext.ValidationNode;
if (currentValidationNode.SuppressValidation) if (currentValidationNode.SuppressValidation)
{ {
// Short circuit if the node is marked to be suppressed // Short circuit if the node is marked to be suppressed.
var validationState = modelState.GetFieldValidationState(modelKey); // If there are any sub entries which were model bound, they need to be marked as skipped,
if (validationState == ModelValidationState.Unvalidated) // Otherwise they will remain as unvalidated and the model state would be Invalid.
{ MarkChildNodesAsSkipped(modelKey, modelExplorer.Metadata, validationContext);
modelValidationContext.ModelState.MarkFieldSkipped(modelKey);
}
// For validation purposes this model is valid. // For validation purposes this model is valid.
return true; return true;
@ -103,7 +101,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
if (IsTypeExcludedFromValidation(_excludeFilters, modelType)) if (IsTypeExcludedFromValidation(_excludeFilters, modelType))
{ {
var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators); var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators);
MarkPropertiesAsSkipped(modelKey, modelExplorer.Metadata, validationContext);
// If there are any sub entries which were model bound, they need to be marked as skipped,
// Otherwise they will remain as unvalidated and the model state would be Invalid.
MarkChildNodesAsSkipped(modelKey, modelExplorer.Metadata, validationContext);
return result; return result;
} }
@ -127,7 +128,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return isValid; return isValid;
} }
private void MarkPropertiesAsSkipped(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext) private void MarkChildNodesAsSkipped(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext)
{ {
var modelState = validationContext.ModelValidationContext.ModelState; var modelState = validationContext.ModelValidationContext.ModelState;
var fieldValidationState = modelState.GetFieldValidationState(currentModelKey); var fieldValidationState = modelState.GetFieldValidationState(currentModelKey);
@ -140,15 +141,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return; return;
} }
foreach (var childMetadata in metadata.Properties) // At this point we just want to mark all sub-entries present in the model state as skipped.
var entries = modelState.FindKeysWithPrefix(currentModelKey);
foreach (var entry in entries)
{ {
var childKey = ModelNames.CreatePropertyModelName(currentModelKey, childMetadata.PropertyName); entry.Value.ValidationState = ModelValidationState.Skipped;
var validationState = modelState.GetFieldValidationState(childKey);
if (validationState == ModelValidationState.Unvalidated)
{
validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(childKey);
}
} }
} }

View File

@ -293,6 +293,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(ModelValidationState.Valid, validationState); Assert.Equal(ModelValidationState.Valid, validationState);
} }
[Theory]
[InlineData("[0].foo.bar")]
[InlineData("[0].foo.bar[0]")]
public void GetFieldValidationState_IndexedPrefix_ReturnsInvalidIfKeyChildContainsErrors(string key)
{
// Arrange
var msd = new ModelStateDictionary();
msd.AddModelError(key, "error text");
// Act
var validationState = msd.GetFieldValidationState("[0].foo");
// Assert
Assert.Equal(ModelValidationState.Invalid, validationState);
}
[Theory]
[InlineData("[0].foo.bar")]
[InlineData("[0].foo.bar[0]")]
public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{
// Arrange
var validState = new ModelState
{
Value = new ValueProviderResult(null, null, null),
ValidationState = ModelValidationState.Valid
};
var msd = new ModelStateDictionary
{
{ key, validState }
};
// Act
var validationState = msd.GetFieldValidationState("[0].foo");
// Assert
Assert.Equal(ModelValidationState.Valid, validationState);
}
[Fact] [Fact]
public void IsValidPropertyReturnsFalseIfErrors() public void IsValidPropertyReturnsFalseIfErrors()
{ {

View File

@ -360,6 +360,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
.Validate(validationContext, topLevelValidationNode); .Validate(validationContext, topLevelValidationNode);
// Assert // Assert
Assert.Equal(1, validationContext.ModelState.Count);
Assert.Contains("Street", validationContext.ModelState.Keys); Assert.Contains("Street", validationContext.ModelState.Keys);
var streetState = validationContext.ModelState["Street"]; var streetState = validationContext.ModelState["Street"];
Assert.Equal(2, streetState.Errors.Count); Assert.Equal(2, streetState.Errors.Count);
@ -427,7 +428,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
validator.Validate(validationContext, topLevelValidationNode); validator.Validate(validationContext, topLevelValidationNode);
// Assert // Assert
Assert.Equal(new[] { "key1", "user.Password", "", "user.ConfirmPassword" }, Assert.Equal(new[] { "key1", "", "user.ConfirmPassword" },
validationContext.ModelState.Keys.ToArray()); validationContext.ModelState.Keys.ToArray());
var modelState = validationContext.ModelState["user.ConfirmPassword"]; var modelState = validationContext.ModelState["user.ConfirmPassword"];
Assert.Empty(modelState.Errors); Assert.Empty(modelState.Errors);
@ -438,7 +439,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
} }
[Fact] [Fact]
public void ForExcludedNonModelBoundType_Properties_NotMarkedAsSkiped() public void ForExcludedNonModelBoundTypes_NoEntryInModelState()
{ {
// Arrange // Arrange
var user = new User() var user = new User()
@ -468,11 +469,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
validator.Validate(validationContext, topLevelValidationNode); validator.Validate(validationContext, topLevelValidationNode);
// Assert // Assert
Assert.False(validationContext.ModelState.ContainsKey("user.Password")); Assert.True(validationContext.ModelState.IsValid);
Assert.False(validationContext.ModelState.ContainsKey("user.ConfirmPassword")); Assert.Empty(validationContext.ModelState);
var modelState = validationContext.ModelState["user"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid);
} }
[Fact] [Fact]
@ -511,13 +509,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
validator.Validate(validationContext, topLevelValidationNode); validator.Validate(validationContext, topLevelValidationNode);
// Assert // Assert
var modelState = validationContext.ModelState["user.Password"]; var entry = Assert.Single(validationContext.ModelState);
Assert.Empty(modelState.Errors); Assert.Equal("user.Password", entry.Key);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); Assert.Empty(entry.Value.Errors);
Assert.Equal(entry.Value.ValidationState, ModelValidationState.Skipped);
}
modelState = validationContext.ModelState["user.ConfirmPassword"]; private class Person2
Assert.Empty(modelState.Errors); {
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); public Address Address { get; set; }
} }
[Fact] [Fact]
@ -525,22 +525,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
// Arrange // Arrange
var testValidationContext = GetModelValidationContext( var testValidationContext = GetModelValidationContext(
new TestServiceProvider(), new Person2()
typeof(TestServiceProvider)); {
Address = new Address { Street = "GreaterThan5Characters" }
},
typeof(Person2));
var validationContext = testValidationContext.ModelValidationContext; var validationContext = testValidationContext.ModelValidationContext;
// Create an entry like a model binder would.
validationContext.ModelState.Add("person.Address", new ModelState());
var validator = new DefaultObjectValidator( var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFilters, testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider); testValidationContext.ModelMetadataProvider);
var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer; var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer;
var topLevelValidationNode = new ModelValidationNode( var topLevelValidationNode = new ModelValidationNode(
"serviceProvider", "person",
modelExplorer.Metadata, modelExplorer.Metadata,
modelExplorer.Model); modelExplorer.Model);
var propertyExplorer = modelExplorer.GetExplorerForProperty("TestService"); var propertyExplorer = modelExplorer.GetExplorerForProperty("Address");
var childNode = new ModelValidationNode( var childNode = new ModelValidationNode(
"serviceProvider.TestService", "person.Address",
propertyExplorer.Metadata, propertyExplorer.Metadata,
propertyExplorer.Model) propertyExplorer.Model)
{ {
@ -554,8 +560,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert // Assert
Assert.True(validationContext.ModelState.IsValid); Assert.True(validationContext.ModelState.IsValid);
var modelState = validationContext.ModelState["serviceProvider.TestService"]; Assert.Equal(1, validationContext.ModelState.Count);
Assert.Empty(modelState.Errors); var modelState = validationContext.ModelState["person.Address"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
} }
@ -603,7 +609,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert // Assert
Assert.True(validationContext.ModelState.IsValid); Assert.True(validationContext.ModelState.IsValid);
var modelState = validationContext.ModelState["items"]; Assert.Equal(3, validationContext.ModelState.Count);
var modelState = validationContext.ModelState["items[0]"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid);
modelState = validationContext.ModelState["items[1]"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid);
modelState = validationContext.ModelState["items[2]"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid); Assert.Equal(modelState.ValidationState, ModelValidationState.Valid);
} }
@ -653,16 +666,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert // Assert
Assert.True(validationContext.ModelState.IsValid); Assert.True(validationContext.ModelState.IsValid);
var modelState = validationContext.ModelState["items"]; Assert.Equal(4, validationContext.ModelState.Count);
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid); var modelState = validationContext.ModelState["items[0].Key"];
modelState = validationContext.ModelState["items[0].Key"]; Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
modelState = validationContext.ModelState["items[0].Value"]; modelState = validationContext.ModelState["items[0].Value"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState);
modelState = validationContext.ModelState["items[1].Key"]; modelState = validationContext.ModelState["items[1].Key"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState);
modelState = validationContext.ModelState["items[1].Value"]; modelState = validationContext.ModelState["items[1].Value"];
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState);
} }
[Fact] [Fact]
@ -690,8 +702,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert // Assert
Assert.True(validationContext.ModelState.IsValid); Assert.True(validationContext.ModelState.IsValid);
var key = Assert.Single(validationContext.ModelState.Keys);
Assert.Equal("person", key); // Since Person is not IValidatable and we do not look at its properties, the state is empty.
Assert.Empty(validationContext.ModelState.Keys);
} }
[Fact] [Fact]

View File

@ -171,7 +171,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
} }
[Fact] [Fact]
public async Task CheckIfExcludedField_IsValidatedForNonBodyBoundModels() public async Task CheckIfExcludedField_IsNotValidatedForNonBodyBoundModels()
{ {
// Arrange // Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices); var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -185,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
//Assert //Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("The Name field is required.", await response.Content.ReadAsStringAsync()); Assert.Equal("xyz", await response.Content.ReadAsStringAsync());
} }
private class ErrorCollection private class ErrorCollection

View File

@ -11,7 +11,9 @@ 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.Controllers;
using ModelBindingWebSite.Models; using ModelBindingWebSite.Models;
using ModelBindingWebSite.ViewModels; using ModelBindingWebSite.ViewModels;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -33,34 +35,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient(); var client = server.CreateClient();
// Act // Act
var response = await client.GetAsync("http://localhost/Validation/DoNotValidateParameter"); var response = await client.GetStringAsync("http://localhost/Validation/DoNotValidateParameter");
// Assert // Assert
var stringValue = await response.Content.ReadAsStringAsync(); var modelState = JsonConvert.DeserializeObject<ModelStateDictionary>(response);
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue); Assert.Empty(modelState);
Assert.True(isModelStateValid); Assert.True(modelState.IsValid);
}
[Fact]
public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Make sure the body object gets created with an invalid zip.
var input = "{\"OfficeAddress.Zip\":\"45\"}";
var content = new StringContent(input, Encoding.UTF8, "application/json");
// Act
// Make sure the non body based object gets created with an invalid zip.
var response = await client.PostAsync(
"http://localhost/Validation/SkipValidation?ShippingAddresses[0].Zip=45&HomeAddress.Zip=46", content);
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
} }
[Fact] [Fact]
@ -76,8 +56,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert // Assert
var stringValue = await response.Content.ReadAsStringAsync(); var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue); var json = JsonConvert.DeserializeObject<ModelStateDictionary>(stringValue);
Assert.True(isModelStateValid); Assert.True(json.IsValid);
} }
[Theory] [Theory]

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext( var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request =>
{ {
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty));
request.ContentType = "application/json"; request.ContentType = "application/json";
}); });
@ -67,7 +67,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
} }
[Fact] [Fact]
public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError() public async Task FromBodyOnActionParameter_EmptyBody_BindsToNullValue()
{ {
// Arrange // Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -85,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext( var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request =>
{ {
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty));
request.ContentType = "application/json"; request.ContentType = "application/json";
}); });
@ -98,13 +98,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Assert // Assert
Assert.NotNull(modelBindingResult); Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet); Assert.True(modelBindingResult.IsModelSet);
var boundPerson = Assert.IsType<Person>(modelBindingResult.Model); Assert.Null(modelBindingResult.Model);
Assert.NotNull(boundPerson); Assert.Empty(modelState.Keys);
var key = Assert.Single(modelState.Keys); Assert.True(modelState.IsValid);
Assert.Equal("Address", key);
Assert.False(modelState.IsValid);
var error = Assert.Single(modelState[key].Errors);
Assert.Equal("The Address field is required.",error.ErrorMessage);
} }
private class Person4 private class Person4

View File

@ -2,6 +2,7 @@
// 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;
using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -597,5 +598,60 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal("Street2", entry.Value.AttemptedValue); Assert.Equal("Street2", entry.Value.AttemptedValue);
Assert.Equal("Street2", entry.Value.RawValue); Assert.Equal("Street2", entry.Value.RawValue);
} }
private class Person5
{
public IList<Address5> Addresses { get; set; }
}
private class Address5
{
public int Zip { get; set; }
[StringLength(3)]
public string Street { get; set; }
}
[Fact]
public async Task CollectionModelBinder_UsesCustomIndexes_AddsErrorsWithCorrectKeys()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person5)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
var formCollection = new FormCollection(new Dictionary<string, string[]>()
{
{ "Addresses.index", new [] { "Key1" } },
{ "Addresses[Key1].Street", new [] { "Street1" } },
});
request.Form = formCollection;
request.ContentType = "application/x-www-form-urlencoded";
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.IsType<Person5>(modelBindingResult.Model);
Assert.Equal(1, modelState.Count);
Assert.Equal(1, modelState.ErrorCount);
Assert.False(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key1].Street").Value;
var error = Assert.Single(entry.Errors);
Assert.Equal("The field Street must be a string with a maximum length of 3.", error.ErrorMessage);
}
} }
} }

View File

@ -49,21 +49,21 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
}; };
} }
public static DefaultControllerActionArgumentBinder GetArgumentBinder() public static DefaultControllerActionArgumentBinder GetArgumentBinder(MvcOptions options = null)
{ {
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
return new DefaultControllerActionArgumentBinder( return new DefaultControllerActionArgumentBinder(
metadataProvider, metadataProvider,
GetObjectValidator()); GetObjectValidator(options));
} }
public static IObjectModelValidator GetObjectValidator() public static IObjectModelValidator GetObjectValidator(MvcOptions options = null)
{ {
var options = new TestMvcOptions(); options = options ?? new TestMvcOptions().Options;
options.Options.MaxModelValidationErrors = 5; options.MaxModelValidationErrors = 5;
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
return new DefaultObjectValidator( return new DefaultObjectValidator(
options.Options.ValidationExcludeFilters, options.ValidationExcludeFilters,
metadataProvider); metadataProvider);
} }

View File

@ -273,7 +273,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
public IScopedInstance<ActionBindingContext> BindingContext { get; set; } public IScopedInstance<ActionBindingContext> BindingContext { get; set; }
} }
[Fact(Skip = "FromServices should not have an entry in model state #2464.")] [Fact]
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_Success() public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_Success()
{ {
// Arrange // Arrange
@ -313,7 +313,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal("bill", entry.Value.RawValue); Assert.Equal("bill", entry.Value.RawValue);
} }
[Fact(Skip = "FromServices should not have an entry in model state #2464.")] [Fact]
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithEmptyPrefix_Success() public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithEmptyPrefix_Success()
{ {
// Arrange // Arrange
@ -355,7 +355,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// We don't provide enough data in this test for the 'Person' model to be created. So even though there is // We don't provide enough data in this test for the 'Person' model to be created. So even though there is
// a [FromServices], it won't be used. // a [FromServices], it won't be used.
[Fact(Skip = "FromServices should not have an entry in model state #2464.")] [Fact]
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_PartialData() public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_PartialData()
{ {
// Arrange // Arrange
@ -427,7 +427,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var model = Assert.IsType<Order2>(modelBindingResult.Model); var model = Assert.IsType<Order2>(modelBindingResult.Model);
Assert.Null(model.Customer); Assert.Null(model.Customer);
Assert.Equal(0, modelState.Count); // Fails due to #2464 Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount); Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
} }

View File

@ -60,15 +60,10 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Empty(modelState.Keys);
Assert.Equal(1, modelState.Keys.Count);
var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.OutputFormatter");
Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState);
Assert.Null(modelState[key].Value);
Assert.Empty(modelState[key].Errors);
} }
[Fact(Skip = "Should be no entry for model bound using services. #2464")] [Fact]
public async Task BindPropertyFromService_WithData_WithEmptyPrefix_GetsBound() public async Task BindPropertyFromService_WithData_WithEmptyPrefix_GetsBound()
{ {
// Arrange // Arrange
@ -99,13 +94,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count);
var key = Assert.Single(modelState.Keys, k => k == "Address"); // For non user bound models there should be no entry in model state.
Assert.Null(modelState[key].Value); // For non user bound models there should be no value. Assert.Empty(modelState);
Assert.Empty(modelState[key].Errors);
} }
[Fact(Skip = "#2464 ModelState should not have entry for non request bound models.")] [Fact]
public async Task BindParameterFromService_WithData_GetsBound() public async Task BindParameterFromService_WithData_GetsBound()
{ {
// Arrange // Arrange

View File

@ -3,7 +3,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
@ -1005,6 +1007,82 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.False(modelState.IsValid); Assert.False(modelState.IsValid);
} }
private class Order11
{
public IEnumerable<Address> ShippingAddresses { get; set; }
public Address HomeAddress { get; set; }
[FromBody]
public Address OfficeAddress { get; set; }
}
private class Address
{
public int Street { get; set; }
public string State { get; set; }
[Range(10000, 99999)]
public int Zip { get; set; }
public Country Country { get; set; }
}
private class Country
{
public string Name { get; set; }
}
[Fact]
public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
{
// Arrange
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Order11)
};
MvcOptions testOptions = null;
var input = "{\"OfficeAddress.Zip\":\"45\"}";
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString =
new QueryString("?HomeAddress.Country.Name=US&ShippingAddresses[0].Zip=45&HomeAddress.Zip=46");
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
request.ContentType = "application/json";
},
options => {
options.ValidationExcludeFilters.Add(typeof(Address));
testOptions = options;
});
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(testOptions);
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
Assert.Equal(3, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Country.Name").Value;
Assert.Equal("US", entry.Value.AttemptedValue);
Assert.Equal("US", entry.Value.RawValue);
Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "ShippingAddresses[0].Zip").Value;
Assert.Equal("45", entry.Value.AttemptedValue);
Assert.Equal("45", entry.Value.RawValue);
Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Zip").Value;
Assert.Equal("46", entry.Value.AttemptedValue);
Assert.Equal("46", entry.Value.RawValue);
Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
}
private static void AssertRequiredError(string key, ModelError error) private static void AssertRequiredError(string key, ModelError error)
{ {
Assert.Equal(string.Format("The {0} field is required.", key), error.ErrorMessage); Assert.Equal(string.Format("The {0} field is required.", key), error.ErrorMessage);

View File

@ -46,7 +46,6 @@ namespace FormatterWebSite
[HttpPost] [HttpPost]
public string GetDeveloperAlias(Developer developer) public string GetDeveloperAlias(Developer developer)
{ {
// Since validation exclusion is currently only effective in case of body bound models.
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
return developer.Alias; return developer.Alias;

View File

@ -14,22 +14,60 @@ namespace ModelBindingWebSite.Controllers
[FromServices] [FromServices]
public ITestService ControllerService { get; set; } public ITestService ControllerService { get; set; }
public bool SkipValidation(Resident resident) public object AvoidRecursive(SelfishPerson selfishPerson)
{ {
return ModelState.IsValid; return new SerializableModelStateDictionary(ModelState);
} }
public bool AvoidRecursive(SelfishPerson selfishPerson) public object DoNotValidateParameter([FromServices] ITestService service)
{ {
return ModelState.IsValid; return ModelState;
}
public bool DoNotValidateParameter([FromServices] ITestService service)
{
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; }