diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index c2f7bd16f2..03afe597eb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -15,7 +15,6 @@ // See the Apache 2 License for the specific language governing // permissions and limitations under the License. -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -44,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private IModelBinder[] Binders { get; set; } - public virtual async Task BindModelAsync(ModelBindingContext bindingContext) + public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) { var newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName, @@ -66,22 +65,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return false; // something went wrong } - // run validation and return the model - // If we fell back to an empty prefix above and are dealing with simple types, - // propagate the non-blank model name through for user clarity in validation errors. - // Complex types will reveal their individual properties as model names and do not require this. - if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName)) + // Only perform validation at the root of the object graph. ValidationNode will recursively walk the graph. + // Ignore ComplexModelDto since it essentially wraps the primary object. + if (newBindingContext.ModelMetadata.ContainerType == null && + newBindingContext.ModelMetadata.ModelType != typeof(ComplexModelDto)) { - newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName); + // run validation and return the model + // If we fell back to an empty prefix above and are dealing with simple types, + // propagate the non-blank model name through for user clarity in validation errors. + // Complex types will reveal their individual properties as model names and do not require this. + if (!newBindingContext.ModelMetadata.IsComplexType && string.IsNullOrEmpty(newBindingContext.ModelName)) + { + newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, + bindingContext.ModelName); + } + + var validationContext = new ModelValidationContext(bindingContext.MetadataProvider, + bindingContext.ValidatorProviders, + bindingContext.ModelState, + bindingContext.ModelMetadata, + containerMetadata: null); + + newBindingContext.ValidationNode.Validate(validationContext, parentNode: null); } - - var validationContext = new ModelValidationContext(bindingContext.MetadataProvider, - bindingContext.ValidatorProviders, - bindingContext.ModelState, - bindingContext.ModelMetadata, - containerMetadata: null); - - newBindingContext.ValidationNode.Validate(validationContext, parentNode: null); + bindingContext.Model = newBindingContext.Model; return true; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs index f062098af4..0ff4fb23ec 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -40,8 +40,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var metadata = validationContext.ModelMetadata; var memberName = metadata.PropertyName ?? metadata.ModelType.Name; - var instance = metadata.Model ?? validationContext.ContainerMetadata.Model; - var context = new ValidationContext(instance) + var containerMetadata = validationContext.ContainerMetadata; + var container = containerMetadata != null ? containerMetadata.Model : null; + var context = new ValidationContext(container ?? metadata.Model) { DisplayName = metadata.GetDisplayName(), MemberName = memberName diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs index 5c3179f3dc..2ddddf69b2 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs @@ -18,6 +18,7 @@ #if NET45 using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Microsoft.Framework.DependencyInjection; @@ -234,10 +235,56 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Equal("name", model.Friends[1].LastName); } + [Fact] + public async Task BindModel_WithDefaultValidators_ValidatesSubProperties() + { + // Arrange + var validatorProvider = new DataAnnotationsModelValidatorProvider(); + var binder = CreateBinderWithDefaults(); + var valueProvider = new SimpleHttpValueProvider + { + { "user.password", "password-val" }, + { "user.confirmpassword", "not-password-val" }, + }; + var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), new[] { validatorProvider }); + bindingContext.ModelName = "user"; + + // Act + var isBound = await binder.BindModelAsync(bindingContext); + + // Assert + var error = Assert.Single(bindingContext.ModelState["user.confirmpassword"].Errors); + Assert.Equal("'ConfirmPassword' and 'Password' do not match.", error.ErrorMessage); + } + + [Fact] + public async Task BindModel_WithDefaultValidators_ValidatesInstance() + { + // Arrange + var validatorProvider = new DataAnnotationsModelValidatorProvider(); + var binder = CreateBinderWithDefaults(); + var valueProvider = new SimpleHttpValueProvider + { + { "user.password", "password" }, + { "user.confirmpassword", "password" }, + }; + var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), new[] { validatorProvider }); + bindingContext.ModelName = "user"; + + // Act + var isBound = await binder.BindModelAsync(bindingContext); + + // Assert + var error = Assert.Single(bindingContext.ModelState["user"].Errors); + Assert.Equal("Password does not meet complexity requirements.", error.ErrorMessage); + } + private static ModelBindingContext CreateBindingContext(IModelBinder binder, IValueProvider valueProvider, - Type type) + Type type, + IEnumerable validatorProviders = null) { + validatorProviders = validatorProviders ?? Enumerable.Empty(); var metadataProvider = new DataAnnotationsModelMetadataProvider(); var bindingContext = new ModelBindingContext { @@ -247,7 +294,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test ModelMetadata = metadataProvider.GetMetadataForType(null, type), ModelState = new ModelStateDictionary(), ValueProvider = valueProvider, - ValidatorProviders = Enumerable.Empty() + ValidatorProviders = validatorProviders }; return bindingContext; } @@ -287,6 +334,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public List Friends { get; set; } } + + private class User : IValidatableObject + { + public string Password { get; set; } + + [Compare("Password")] + public string ConfirmPassword { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Password == "password") + { + yield return new ValidationResult("Password does not meet complexity requirements."); + } + } + } } } #endif