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
# Visual Studio 14
VisualStudioVersion = 14.0.22810.0
VisualStudioVersion = 14.0.22808.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -166,6 +166,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExp
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}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1077,5 +1091,6 @@ Global
{1154203C-7579-4525-906E-BC55268421C1} = {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}
{DA000953-7532-4DF5-8DB9-8143DF98D999} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection
EndGlobal

View File

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

View File

@ -66,12 +66,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var currentValidationNode = validationContext.ValidationNode;
if (currentValidationNode.SuppressValidation)
{
// Short circuit if the node is marked to be suppressed
var validationState = modelState.GetFieldValidationState(modelKey);
if (validationState == ModelValidationState.Unvalidated)
{
modelValidationContext.ModelState.MarkFieldSkipped(modelKey);
}
// Short circuit if the node is marked to be suppressed.
// 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);
// For validation purposes this model is valid.
return true;
@ -103,7 +101,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
if (IsTypeExcludedFromValidation(_excludeFilters, modelType))
{
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;
}
@ -127,7 +128,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
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 fieldValidationState = modelState.GetFieldValidationState(currentModelKey);
@ -140,15 +141,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
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);
var validationState = modelState.GetFieldValidationState(childKey);
if (validationState == ModelValidationState.Unvalidated)
{
validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(childKey);
}
entry.Value.ValidationState = ModelValidationState.Skipped;
}
}

View File

@ -293,6 +293,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
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]
public void IsValidPropertyReturnsFalseIfErrors()
{

View File

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

View File

@ -171,7 +171,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
}
[Fact]
public async Task CheckIfExcludedField_IsValidatedForNonBodyBoundModels()
public async Task CheckIfExcludedField_IsNotValidatedForNonBodyBoundModels()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -185,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
//Assert
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

View File

@ -11,7 +11,9 @@ 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.Controllers;
using ModelBindingWebSite.Models;
using ModelBindingWebSite.ViewModels;
using Newtonsoft.Json;
@ -33,34 +35,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/Validation/DoNotValidateParameter");
var response = await client.GetStringAsync("http://localhost/Validation/DoNotValidateParameter");
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
}
[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);
var modelState = JsonConvert.DeserializeObject<ModelStateDictionary>(response);
Assert.Empty(modelState);
Assert.True(modelState.IsValid);
}
[Fact]
@ -76,8 +56,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
var json = JsonConvert.DeserializeObject<ModelStateDictionary>(stringValue);
Assert.True(json.IsValid);
}
[Theory]

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request =>
{
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }"));
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty));
request.ContentType = "application/json";
});
@ -67,7 +67,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
}
[Fact]
public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError()
public async Task FromBodyOnActionParameter_EmptyBody_BindsToNullValue()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -85,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request =>
{
request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }"));
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty));
request.ContentType = "application/json";
});
@ -98,13 +98,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
var boundPerson = Assert.IsType<Person>(modelBindingResult.Model);
Assert.NotNull(boundPerson);
var key = Assert.Single(modelState.Keys);
Assert.Equal("Address", key);
Assert.False(modelState.IsValid);
var error = Assert.Single(modelState[key].Errors);
Assert.Equal("The Address field is required.",error.ErrorMessage);
Assert.Null(modelBindingResult.Model);
Assert.Empty(modelState.Keys);
Assert.True(modelState.IsValid);
}
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.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@ -597,5 +598,60 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal("Street2", entry.Value.AttemptedValue);
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();
return new DefaultControllerActionArgumentBinder(
metadataProvider,
GetObjectValidator());
GetObjectValidator(options));
}
public static IObjectModelValidator GetObjectValidator()
public static IObjectModelValidator GetObjectValidator(MvcOptions options = null)
{
var options = new TestMvcOptions();
options.Options.MaxModelValidationErrors = 5;
options = options ?? new TestMvcOptions().Options;
options.MaxModelValidationErrors = 5;
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
return new DefaultObjectValidator(
options.Options.ValidationExcludeFilters,
options.ValidationExcludeFilters,
metadataProvider);
}

View File

@ -273,7 +273,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
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()
{
// Arrange
@ -313,7 +313,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
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()
{
// 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
// 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()
{
// Arrange
@ -427,7 +427,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var model = Assert.IsType<Order2>(modelBindingResult.Model);
Assert.Null(model.Customer);
Assert.Equal(0, modelState.Count); // Fails due to #2464
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}

View File

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

View File

@ -3,7 +3,9 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -1005,6 +1007,82 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
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)
{
Assert.Equal(string.Format("The {0} field is required.", key), error.ErrorMessage);

View File

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

View File

@ -14,22 +14,60 @@ namespace ModelBindingWebSite.Controllers
[FromServices]
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;
}
public bool DoNotValidateParameter([FromServices] ITestService service)
{
return ModelState.IsValid;
return ModelState;
}
}
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; }