Make validation in `TryUpdateModelAsync()` consistent with model binding elsewhere

- #2941
- honor `ModelBindingResult.IsModelSet` and use `ModelBindingResult.ValidationNode`
  - enable correct validation of collections or after model binding falls back to the empty prefix
  - code previously matched `Controller.TryValidateModel()`; less context available in that case
This commit is contained in:
Doug Bunting 2015-08-20 22:36:26 -07:00
parent b5c9d905d9
commit 070be7b656
3 changed files with 118 additions and 37 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@ -306,16 +307,18 @@ namespace Microsoft.AspNet.Mvc
};
var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext);
if (modelBindingResult != null)
if (modelBindingResult != null && modelBindingResult.IsModelSet)
{
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model);
var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer);
objectModelValidator.Validate(
modelValidationContext,
new ModelValidationNode(prefix, modelBindingContext.ModelMetadata, modelBindingResult.Model)
{
ValidateAllProperties = true
});
var validationNode = modelBindingResult.ValidationNode;
Debug.Assert(
validationNode != null,
"ValidationNode should never be null in a successful ModelBindingResult.");
objectModelValidator.Validate(modelValidationContext, validationNode);
return modelState.IsValid;
}

View File

@ -17,15 +17,29 @@ namespace Microsoft.AspNet.Mvc.Test
{
public class ModelBindingHelperTest
{
[Fact]
public async Task TryUpdateModel_ReturnsFalse_IfBinderReturnsNull()
public static TheoryData<ModelBindingResult> UnsuccessfulModelBindingData
{
get
{
return new TheoryData<ModelBindingResult>
{
null,
new ModelBindingResult("someKey"), // IsFatalError true as well as IsModelSet false.
new ModelBindingResult(model: null, key: "someKey", isModelSet: false),
};
}
}
[Theory]
[MemberData(nameof(UnsuccessfulModelBindingData))]
public async Task TryUpdateModel_ReturnsFalse_IfBinderIsUnsuccessful(ModelBindingResult binderResult)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(binderResult));
var model = new MyModel();
// Act
@ -126,18 +140,20 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("MyPropertyValue", model.MyProperty);
}
[Fact]
public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsFalse_IfBinderReturnsNull()
[Theory]
[MemberData(nameof(UnsuccessfulModelBindingData))]
public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsFalse_IfBinderIsUnsuccessful(
ModelBindingResult binderResult)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(binderResult));
var model = new MyModel();
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) => true;
Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) => true;
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
@ -214,15 +230,17 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("Old-ExcludedPropertyValue", model.ExcludedProperty);
}
[Fact]
public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsFalse_IfBinderReturnsNull()
[Theory]
[MemberData(nameof(UnsuccessfulModelBindingData))]
public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsFalse_IfBinderIsUnsuccessful(
ModelBindingResult binderResult)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(binderResult));
var model = new MyModel();
// Act
@ -468,17 +486,20 @@ namespace Microsoft.AspNet.Mvc.Test
ex.Message);
}
[Fact]
public async Task TryUpdateModelNonGeneric_PredicateOverload_ReturnsFalse_IfBinderReturnsNull()
[Theory]
[MemberData(nameof(UnsuccessfulModelBindingData))]
public async Task TryUpdateModelNonGeneric_PredicateOverload_ReturnsFalse_IfBinderIsUnsuccessful(
ModelBindingResult binderResult)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(binderResult));
var model = new MyModel();
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) => true;
Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) => true;
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
@ -560,15 +581,17 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("Old-ExcludedPropertyValue", model.ExcludedProperty);
}
[Fact]
public async Task TryUpdateModelNonGeneric_ModelTypeOverload_ReturnsFalse_IfBinderReturnsNull()
[Theory]
[MemberData(nameof(UnsuccessfulModelBindingData))]
public async Task TryUpdateModelNonGeneric_ModelTypeOverload_ReturnsFalse_IfBinderIsUnsuccessful(
ModelBindingResult binderResult)
{
// Arrange
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(binderResult));
var model = new MyModel();
// Act

View File

@ -102,6 +102,63 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
public Address Address { get; set; }
}
[Fact]
public async Task TryUpdateModel_TopLevelCollection_EmptyPrefix_BindsAfterClearing()
{
// Arrange
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = QueryString.Create(new Dictionary<string, string>
{
{ "[0].Name", "One Name" },
{ "[1].Address.Street", "Two Street" },
});
});
var modelState = new ModelStateDictionary();
var model = new List<Person1>
{
new Person1
{
Name = "One",
Address = new Address
{
Street = "DefaultStreet",
City = "Toronto",
},
},
new Person1 { Name = "Two" },
new Person1 { Name = "Three" },
};
// Act
var result = await TryUpdateModel(model, string.Empty, operationContext, modelState);
// Assert
Assert.True(result);
// Model
Assert.Collection(model,
element =>
{
Assert.Equal("One Name", element.Name);
Assert.Null(element.Address);
},
element =>
{
Assert.Null(element.Name);
Assert.NotNull(element.Address);
Assert.Equal("Two Street", element.Address.Street);
Assert.Null(element.Address.City);
});
// ModelState
Assert.True(modelState.IsValid);
Assert.Equal(2, modelState.Count);
Assert.NotNull(modelState["[0].Name"]);
Assert.NotNull(modelState["[1].Address.Street"]);
}
[Fact]
public async Task TryUpdateModel_NestedPoco_EmptyPrefix_DoesNotTrounceUnboundValues()
{
@ -299,7 +356,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
public CustomReadOnlyCollection<Address> Address { get; set; }
}
[Fact(Skip = "Validation incorrect for collections when using TryUpdateModel, #2941")]
public async Task TryUpdateModel_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound()
{
// Arrange
@ -712,7 +768,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
}
[Fact(Skip = "Validation incorrect for collections when using TryUpdateModel, #2941")]
public async Task TryUpdateModel_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound()
{
// Arrange