From f8dd52dfe3bc986dd2f1d7e7576d1cb426d0d121 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 17 Mar 2014 15:04:14 -0700 Subject: [PATCH] Reintroducing BindingBehaviorAttribute attribute * Porting MutableObjectModelBinder tests * Fix issues in MutableObjectModelBinder introduced by changing from TypeDescriptors to reflection --- .../Binders/BindNeverAttribute.cs | 13 + .../Binders/BindRequiredAttribute.cs | 13 + .../Binders/BindingBehavior.cs | 10 + .../Binders/BindingBehaviorAttribute.cs | 15 + .../Binders/CompositeModelBinder.cs | 9 +- .../Binders/MutableObjectModelBinder.cs | 115 ++- .../Internal/ModelBindingContextExtensions.cs | 11 +- .../Metadata/AssociatedMetadataProvider.cs | 31 +- .../DataAnnotationsModelValidator.cs | 3 +- .../Validation/ModelValidationContext.cs | 22 +- .../Binders/MutableObjectModelBinderTest.cs | 953 ++++++++++++++++++ ...taAnnotationsModelValidatorProviderTest.cs | 2 +- .../DataAnnotationsModelValidatorTest.cs | 2 +- .../Validation/ModelValidationNodeTest.cs | 7 +- 14 files changed, 1130 insertions(+), 76 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindNeverAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindRequiredAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehavior.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehaviorAttribute.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindNeverAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindNeverAttribute.cs new file mode 100644 index 0000000000..f25f5e6ffe --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindNeverAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class BindNeverAttribute : BindingBehaviorAttribute + { + public BindNeverAttribute() + : base(BindingBehavior.Never) + { + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindRequiredAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindRequiredAttribute.cs new file mode 100644 index 0000000000..ef8c1aabb0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindRequiredAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class BindRequiredAttribute : BindingBehaviorAttribute + { + public BindRequiredAttribute() + : base(BindingBehavior.Required) + { + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehavior.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehavior.cs new file mode 100644 index 0000000000..007e085420 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehavior.cs @@ -0,0 +1,10 @@ + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public enum BindingBehavior + { + Optional = 0, + Never, + Required + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehaviorAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehaviorAttribute.cs new file mode 100644 index 0000000000..01816ef246 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingBehaviorAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class BindingBehaviorAttribute : Attribute + { + public BindingBehaviorAttribute(BindingBehavior behavior) + { + Behavior = behavior; + } + + public BindingBehavior Behavior { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index 37db668822..d2fc553a3e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -57,10 +57,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName); } - var validationContext = new ModelValidationContext(bindingContext.ModelMetadata, - bindingContext.ModelState, - bindingContext.MetadataProvider, - bindingContext.ValidatorProviders); + var validationContext = new ModelValidationContext(bindingContext.MetadataProvider, + bindingContext.ValidatorProviders, + bindingContext.ModelState, + bindingContext.ModelMetadata, + containerMetadata: null); newBindingContext.ValidationNode.Validate(validationContext, parentNode: null); bindingContext.Model = newBindingContext.Model; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 307a5a5d6e..3e898e3400 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { // TODO: Revive ModelBinderConfig // string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext, modelMetadata, incomingValue); - var errorMessage = e.ValidationContext.ModelMetadata.PropertyName + " is required"; + var errorMessage = "A value is required."; if (errorMessage != null) { modelState.AddModelError(validationNode.ModelStateKey, errorMessage); @@ -136,17 +136,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding protected virtual IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) { - // keep a set of the required properties so that we can cross-reference bound properties later - HashSet requiredProperties; - Dictionary requiredValidators; - HashSet skipProperties; - GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties); - - return from propertyMetadata in bindingContext.ModelMetadata.Properties - let propertyName = propertyMetadata.PropertyName - let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName) - where shouldUpdateProperty && CanUpdateProperty(propertyMetadata) - select propertyMetadata; + var validationInfo = GetPropertyValidationInfo(bindingContext); + return bindingContext.ModelMetadata.Properties + .Where(propertyMetadata => + (validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) || + !validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) && + CanUpdateProperty(propertyMetadata)); } private static object GetPropertyDefaultValue(PropertyInfo propertyInfo) @@ -155,43 +150,55 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return (attr != null) ? attr.Value : null; } - internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext, - out HashSet requiredProperties, - out Dictionary requiredValidators, - out HashSet skipProperties) + internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext) { - requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - requiredValidators = new Dictionary(StringComparer.OrdinalIgnoreCase); - skipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - - // TODO: HttpBindingBehaviorAttribute + var validationInfo = new PropertyValidationInfo(); var modelTypeInfo = bindingContext.ModelType.GetTypeInfo(); - foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties) + var typeAttribute = modelTypeInfo.GetCustomAttribute(); + var properties = bindingContext.ModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (var property in properties) { - var propertyName = propertyMetadata.PropertyName; + var propertyName = property.Name; + var propertyMetadata = bindingContext.PropertyMetadata[propertyName]; var requiredValidator = bindingContext.GetValidators(propertyMetadata) .FirstOrDefault(v => v.IsRequired); - // TODO: Revive HttpBindingBehaviorAttribute - if (requiredValidator != null) { - requiredValidators[propertyName] = requiredValidator; - requiredProperties.Add(propertyName); + validationInfo.RequiredValidators[propertyName] = requiredValidator; + } + + var propertyAttribute = property.GetCustomAttribute(); + var bindingBehaviorAttribute = propertyAttribute ?? typeAttribute; + if (bindingBehaviorAttribute != null) + { + switch (bindingBehaviorAttribute.Behavior) + { + case BindingBehavior.Required: + validationInfo.RequiredProperties.Add(propertyName); + break; + + case BindingBehavior.Never: + validationInfo.SkipProperties.Add(propertyName); + break; + } + } + else if (requiredValidator != null) + { + validationInfo.RequiredProperties.Add(propertyName); } } + + return validationInfo; } internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto) { - HashSet requiredProperties; - Dictionary requiredValidators; - HashSet skipProperties; - GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out requiredValidators, out skipProperties); - + var validationInfo = GetPropertyValidationInfo(bindingContext); + // Eliminate provided properties from requiredProperties; leaving just *missing* required properties. - requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); + validationInfo.RequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); - foreach (var missingRequiredProperty in requiredProperties) + foreach (var missingRequiredProperty in validationInfo.RequiredProperties) { var addedError = false; var modelStateKey = ModelBindingHelper.CreatePropertyModelName( @@ -205,12 +212,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Execute validator (if any) to get custom error message. IModelValidator validator; - if (requiredValidators.TryGetValue(missingRequiredProperty, out validator)) + if (validationInfo.RequiredValidators.TryGetValue(missingRequiredProperty, out validator)) { addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey); } - // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator + // Fall back to default message if BindingBehaviorAttribute required this property or validator // (oddly) succeeded. if (!addedError) { @@ -227,7 +234,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var dtoResult = entry.Value; if (dtoResult != null) { - SetProperty(bindingContext, propertyMetadata, dtoResult); + IModelValidator requiredValidator; + validationInfo.RequiredValidators.TryGetValue(propertyMetadata.PropertyName, out requiredValidator); + SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode); } } @@ -236,7 +245,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] protected virtual void SetProperty(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, - ComplexModelDtoResult dtoResult) + ComplexModelDtoResult dtoResult, + IModelValidator requiredValidator) { var property = bindingContext.ModelType .GetProperty(propertyMetadata.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); @@ -258,10 +268,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var modelStateKey = dtoResult.ValidationNode.ModelStateKey; if (bindingContext.ModelState.IsValidField(modelStateKey)) { - var requiredValidator = bindingContext.GetValidators(propertyMetadata).FirstOrDefault(v => v.IsRequired); if (requiredValidator != null) { - var validationContext = bindingContext.CreateValidationContext(propertyMetadata); + var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); foreach (var validationResult in requiredValidator.Validate(validationContext)) { bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); @@ -279,10 +288,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding catch (Exception ex) { // don't display a duplicate error message if a binding error has already occurred for this field + var targetInvocationException = ex as TargetInvocationException; + if (targetInvocationException != null && + targetInvocationException.InnerException != null) + { + ex = targetInvocationException.InnerException; + } var modelStateKey = dtoResult.ValidationNode.ModelStateKey; if (bindingContext.ModelState.IsValidField(modelStateKey)) { - bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex); + bindingContext.ModelState.AddModelError(modelStateKey, ex); } } } @@ -303,7 +318,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding ModelMetadata propertyMetadata, string modelStateKey) { - var validationContext = bindingContext.CreateValidationContext(propertyMetadata); + var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); var addedError = false; foreach (var validationResult in validator.Validate(validationContext)) @@ -313,5 +328,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } return addedError; } + + internal sealed class PropertyValidationInfo + { + public PropertyValidationInfo() + { + RequiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + RequiredValidators = new Dictionary(StringComparer.OrdinalIgnoreCase); + SkipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + public HashSet RequiredProperties { get; private set; } + + public Dictionary RequiredValidators { get; private set; } + + public HashSet SkipProperties { get; private set; } + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs index b35c51203d..f4a6847a9c 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingContextExtensions.cs @@ -12,20 +12,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal .Where(v => v != null); } - public static ModelValidationContext CreateValidationContext([NotNull] this ModelBindingContext context, - [NotNull] ModelMetadata metadata) - { - return new ModelValidationContext(metadata, - context.ModelState, - context.MetadataProvider, - context.ValidatorProviders); - } - public static IEnumerable Validate([NotNull] this ModelBindingContext bindingContext) { var validators = GetValidators(bindingContext, bindingContext.ModelMetadata); var compositeValidator = new CompositeModelValidator(validators); - var modelValidationContext = CreateValidationContext(bindingContext, bindingContext.ModelMetadata); + var modelValidationContext = new ModelValidationContext(bindingContext, bindingContext.ModelMetadata); return compositeValidator.Validate(modelValidationContext); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index 6418f63ad9..78997c3c81 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding throw Error.Argument("propertyName", Resources.FormatCommon_PropertyNotFound(containerType, propertyName)); } - return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); + return CreatePropertyMetadata(modelAccessor, propertyInfo); } public ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType) @@ -64,10 +64,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Func propertyGetter = propertyInfo.ValueAccessor; modelAccessor = () => propertyGetter(container); } - yield return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); + yield return CreatePropertyMetadata(modelAccessor, propertyInfo); } } + private TModelMetadata CreatePropertyMetadata(Func modelAccessor, PropertyInformation propertyInfo) + { + var metadata = CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); + if (propertyInfo.IsReadOnly) + { + metadata.IsReadOnly = true; + } + return metadata; + } + private TypeInformation GetTypeInformation(Type type, IEnumerable associatedAttributes = null) { // This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd to avoid the performance cost of creating instance delegates @@ -109,13 +119,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property) { - var info = new PropertyInformation(); - info.ValueAccessor = CreatePropertyValueAccessor(property); - info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes(), - containerType, - property.PropertyType, - property.Name); - return info; + return new PropertyInformation + { + ValueAccessor = CreatePropertyValueAccessor(property), + Prototype = CreateMetadataPrototype(property.GetCustomAttributes(), + containerType, + property.PropertyType, + property.Name), + IsReadOnly = !property.CanWrite || property.SetMethod.IsPrivate + }; } private static Func CreatePropertyValueAccessor(PropertyInfo property) @@ -196,6 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public Func ValueAccessor { get; set; } public TModelMetadata Prototype { get; set; } + public bool IsReadOnly { get; set; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs index 3108e49146..79225d8a09 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -28,7 +28,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var metadata = validationContext.ModelMetadata; var memberName = metadata.PropertyName ?? metadata.ModelType.Name; - var context = new ValidationContext(metadata.Model) + var instance = metadata.Model ?? validationContext.ContainerMetadata.Model; + var context = new ValidationContext(instance) { DisplayName = metadata.GetDisplayName(), MemberName = memberName diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs index 65d9daa814..9807c9186e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationContext.cs @@ -4,18 +4,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public class ModelValidationContext { - public ModelValidationContext([NotNull] ModelMetadata metadata, - [NotNull] ModelStateDictionary modelState, - [NotNull] IModelMetadataProvider metadataProvider, - [NotNull] IEnumerable validatorProviders) + public ModelValidationContext([NotNull] ModelBindingContext bindingContext, + [NotNull] ModelMetadata metadata) + : this(bindingContext.MetadataProvider, + bindingContext.ValidatorProviders, + bindingContext.ModelState, + metadata, + bindingContext.ModelMetadata) + { + } + + public ModelValidationContext([NotNull] IModelMetadataProvider metadataProvider, + [NotNull] IEnumerable validatorProviders, + [NotNull] ModelStateDictionary modelState, + [NotNull] ModelMetadata metadata, + ModelMetadata containerMetadata) { ModelMetadata = metadata; ModelState = modelState; MetadataProvider = metadataProvider; ValidatorProviders = validatorProviders; + ContainerMetadata = containerMetadata; } - public ModelValidationContext([NotNull] ModelValidationContext parentContext, + public ModelValidationContext([NotNull] ModelValidationContext parentContext, [NotNull] ModelMetadata metadata) { ModelMetadata = metadata; diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs new file mode 100644 index 0000000000..aded08fe6f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -0,0 +1,953 @@ +#if NET45 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class MutableObjectModelBinderTest + { + [Fact] + public void BindModel_InitsInstance() + { + // Arrange + var mockValueProvider = new Mock(); + mockValueProvider.Setup(o => o.ContainsPrefix(It.IsAny())) + .Returns(true); + + var mockDtoBinder = new Mock(); + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForObject(new Person()), + ModelName = "someName", + ValueProvider = mockValueProvider.Object, + ModelBinder = mockDtoBinder.Object, + MetadataProvider = new DataAnnotationsModelMetadataProvider(), + ValidatorProviders = Enumerable.Empty() + }; + + mockDtoBinder + .Setup(o => o.BindModel(It.IsAny())) + .Returns((ModelBindingContext mbc) => + { + // just return the DTO unchanged + return true; + }); + + var testableBinder = new Mock { CallBase = true }; + testableBinder.Setup(o => o.EnsureModelPublic(bindingContext)).Verifiable(); + testableBinder.Setup(o => o.GetMetadataForPropertiesPublic(bindingContext)) + .Returns(new ModelMetadata[0]).Verifiable(); + + // Act + var retValue = testableBinder.Object.BindModel(bindingContext); + + // Assert + Assert.True(retValue); + Assert.IsType(bindingContext.Model); + Assert.True(bindingContext.ValidationNode.ValidateAllProperties); + testableBinder.Verify(); + } + + [Fact] + public void CanUpdateProperty_HasPublicSetter_ReturnsTrue() + { + // Arrange + var propertyMetadata = GetMetadataForCanUpdateProperty("ReadWriteString"); + + // Act + var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); + + // Assert + Assert.True(canUpdate); + } + + [Fact] + public void CanUpdateProperty_ReadOnlyArray_ReturnsFalse() + { + // Arrange + var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyArray"); + + // Act + var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); + + // Assert + Assert.False(canUpdate); + } + + [Fact] + public void CanUpdateProperty_ReadOnlyReferenceTypeNotBlacklisted_ReturnsTrue() + { + // Arrange + var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyObject"); + + // Act + var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); + + // Assert + Assert.True(canUpdate); + } + + [Fact] + public void CanUpdateProperty_ReadOnlyString_ReturnsFalse() + { + // Arrange + var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyString"); + + // Act + var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); + + // Assert + Assert.False(canUpdate); + } + + [Fact] + public void CanUpdateProperty_ReadOnlyValueType_ReturnsFalse() + { + // Arrange + var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyInt"); + + // Act + var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); + + // Assert + Assert.False(canUpdate); + } + + [Fact] + public void CreateModel_InstantiatesInstanceOfMetadataType() + { + // Arrange + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(typeof(Person)) + }; + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + var retModel = testableBinder.CreateModelPublic(bindingContext); + + // Assert + Assert.IsType(retModel); + } + + [Fact] + public void EnsureModel_ModelIsNotNull_DoesNothing() + { + // Arrange + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForObject(new Person()) + }; + + var testableBinder = new Mock { CallBase = true }; + + // Act + var originalModel = bindingContext.Model; + testableBinder.Object.EnsureModelPublic(bindingContext); + var newModel = bindingContext.Model; + + // Assert + Assert.Same(originalModel, newModel); + testableBinder.Verify(o => o.CreateModelPublic(bindingContext), Times.Never()); + } + + [Fact] + public void EnsureModel_ModelIsNull_CallsCreateModel() + { + // Arrange + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(typeof(Person)) + }; + + var testableBinder = new Mock { CallBase = true }; + testableBinder.Setup(o => o.CreateModelPublic(bindingContext)) + .Returns(new Person()).Verifiable(); + + // Act + object originalModel = bindingContext.Model; + testableBinder.Object.EnsureModelPublic(bindingContext); + object newModel = bindingContext.Model; + + // Assert + Assert.Null(originalModel); + Assert.IsType(newModel); + testableBinder.Verify(); + } + + [Fact] + public void GetMetadataForProperties_WithBindAttribute() + { + // Arrange + var expectedPropertyNames = new[] { "FirstName", "LastName" }; + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)), + ValidatorProviders = Enumerable.Empty() + }; + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext); + var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray(); + + // Assert + Assert.Equal(expectedPropertyNames, returnedPropertyNames); + } + + [Fact] + public void GetMetadataForProperties_WithoutBindAttribute() + { + // Arrange + var expectedPropertyNames = new[] { "DateOfBirth", "DateOfDeath", "ValueTypeRequired", "FirstName", "LastName", "PropertyWithDefaultValue" }; + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForType(typeof(Person)), + ValidatorProviders = Enumerable.Empty() + }; + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + var propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(bindingContext); + var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray(); + + // Assert + Assert.Equal(expectedPropertyNames, returnedPropertyNames); + } + + [Fact] + public void GetRequiredPropertiesCollection_MixedAttributes() + { + // Arrange + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetMetadataForObject(new ModelWithMixedBindingBehaviors()), + ValidatorProviders = Enumerable.Empty() + }; + + // Act + var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext); + + // Assert + Assert.Equal(new[] { "Required" }, validationInfo.RequiredProperties); + Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties); + } + + [Fact] + public void NullCheckFailedHandler_ModelStateAlreadyInvalid_DoesNothing() + { + // Arrange + var modelState = new ModelStateDictionary(); + modelState.AddModelError("foo.bar", "Some existing error."); + + var modelMetadata = GetMetadataForType(typeof(Person)); + var validationNode = new ModelValidationNode(modelMetadata, "foo"); + var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(), + Enumerable.Empty(), + modelState, + modelMetadata, + null); + var e = new ModelValidatedEventArgs(validationContext, parentNode: null); + + // Act + var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null); + handler(validationNode, e); + + // Assert + Assert.False(modelState.ContainsKey("foo")); + } + + [Fact] + public void NullCheckFailedHandler_ModelStateValid_AddsErrorString() + { + // Arrange + var modelState = new ModelStateDictionary(); + var modelMetadata = GetMetadataForType(typeof(Person)); + var validationNode = new ModelValidationNode(modelMetadata, "foo"); + var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(), + Enumerable.Empty(), + modelState, + modelMetadata, + null); + var e = new ModelValidatedEventArgs(validationContext, parentNode: null); + + // Act + var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null); + handler(validationNode, e); + + // Assert + Assert.True(modelState.ContainsKey("foo")); + Assert.Equal("A value is required.", modelState["foo"].Errors[0].ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void ProcessDto_BindRequiredFieldMissing_RaisesModelError() + { + // Arrange + var model = new ModelWithBindRequired + { + Name = "original value", + Age = -20 + }; + + var containerMetadata = GetMetadataForObject(model); + var bindingContext = new ModelBindingContext + { + ModelMetadata = containerMetadata, + ModelName = "theModel", + ValidatorProviders = Enumerable.Empty() + }; + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + + var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); + dto.Results[nameProperty] = new ComplexModelDtoResult("John Doe", new ModelValidationNode(nameProperty, "")); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + var modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(1, modelStateDictionary.Count); + + // Check Age error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + var modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("The 'Age' property is required.", modelError.ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void ProcessDto_BindRequiredFieldNull_RaisesModelError() + { + // Arrange + var model = new ModelWithBindRequired + { + Name = "original value", + Age = -20 + }; + + var containerMetadata = GetMetadataForObject(model); + var bindingContext = new ModelBindingContext() + { + ModelMetadata = containerMetadata, + ModelName = "theModel", + ModelState = new ModelStateDictionary(), + ValidatorProviders = Enumerable.Empty() + }; + var validationContext = new ModelValidationContext(new EmptyModelMetadataProvider(), + bindingContext.ValidatorProviders, + bindingContext.ModelState, + containerMetadata, + null); + + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + var testableBinder = new TestableMutableObjectModelBinder(); + + var propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); + dto.Results[propertyMetadata] = + new ComplexModelDtoResult("John Doe", new ModelValidationNode(propertyMetadata, "theModel.Name")); + + // Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this + // case because the binding exists. + propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Age"); + dto.Results[propertyMetadata] = + new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.Age")); + + // Act; must also Validate because null-check error handler is late-bound + testableBinder.ProcessDto(bindingContext, dto); + bindingContext.ValidationNode.Validate(validationContext); + + // Assert + var modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(1, modelStateDictionary.Count); + + // Check Age error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + var modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("A value is required.", modelError.ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void ProcessDto_RequiredFieldMissing_RaisesModelError() + { + // Arrange + var model = new ModelWithRequired(); + var containerMetadata = GetMetadataForObject(model); + var bindingContext = CreateContext(containerMetadata); + + // Set no properties though Age (a non-Nullable struct) and City (a class) properties are required. + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + var modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(2, modelStateDictionary.Count); + + // Check Age error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + var modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("The Age field is required.", modelError.ErrorMessage); + + // Check City error. + Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("The City field is required.", modelError.ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void ProcessDto_RequiredFieldNull_RaisesModelError() + { + // Arrange + var model = new ModelWithRequired(); + var containerMetadata = GetMetadataForObject(model); + var bindingContext = CreateContext(containerMetadata); + + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + var testableBinder = new TestableMutableObjectModelBinder(); + + // Make Age valid and City invalid. + var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "Age"); + dto.Results[propertyMetadata] = + new ComplexModelDtoResult(23, new ModelValidationNode(propertyMetadata, "theModel.Age")); + propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "City"); + dto.Results[propertyMetadata] = + new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.City")); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + var modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(1, modelStateDictionary.Count); + + // Check City error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + var modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("The City field is required.", modelError.ErrorMessage); + } + + [Fact] + public void ProcessDto_RequiredFieldMissing_RaisesModelErrorWithMessage() + { + // Arrange + var model = new Person(); + var containerMetadata = GetMetadataForObject(model); + var bindingContext = CreateContext(containerMetadata); + + // Set no properties though ValueTypeRequired (a non-Nullable struct) property is required. + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + var modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(1, modelStateDictionary.Count); + + // Check ValueTypeRequired error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + var modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("Sample message", modelError.ErrorMessage); + } + + [Fact] + public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage() + { + // Arrange + var model = new Person(); + var containerMetadata = GetMetadataForObject(model); + + var bindingContext = CreateContext(containerMetadata); + + ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder(); + + // Make ValueTypeRequired invalid. + var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired"); + dto.Results[propertyMetadata] = + new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.ValueTypeRequired")); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + ModelStateDictionary modelStateDictionary = bindingContext.ModelState; + Assert.False(modelStateDictionary.IsValid); + Assert.Equal(1, modelStateDictionary.Count); + + // Check ValueTypeRequired error. + ModelState modelState; + Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState)); + Assert.Equal(1, modelState.Errors.Count); + + ModelError modelError = modelState.Errors[0]; + Assert.Null(modelError.Exception); + Assert.NotNull(modelError.ErrorMessage); + Assert.Equal("Sample message", modelError.ErrorMessage); + } + + [Fact] + public void ProcessDto_Success() + { + // Arrange + var dob = new DateTime(2001, 1, 1); + var model = new PersonWithBindExclusion + { + DateOfBirth = dob + }; + var containerMetadata = GetMetadataForObject(model); + + var bindingContext = CreateContext(containerMetadata); + var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); + + var firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName"); + dto.Results[firstNameProperty] = new ComplexModelDtoResult("John", new ModelValidationNode(firstNameProperty, "")); + var lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName"); + dto.Results[lastNameProperty] = new ComplexModelDtoResult("Doe", new ModelValidationNode(lastNameProperty, "")); + var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth"); + dto.Results[dobProperty] = null; + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + Assert.Equal("John", model.FirstName); + Assert.Equal("Doe", model.LastName); + Assert.Equal(dob, model.DateOfBirth); + Assert.True(bindingContext.ModelState.IsValid); + } + + [Fact] + public void SetProperty_PropertyHasDefaultValue_SetsDefaultValue() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + + var propertyMetadata = bindingContext.ModelMetadata.Properties.First(o => o.PropertyName == "PropertyWithDefaultValue"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var requiredValidator = bindingContext.ValidatorProviders + .SelectMany(v => v.GetValidators(propertyMetadata)) + .Where(v => v.IsRequired) + .FirstOrDefault(); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + var person = Assert.IsType(bindingContext.Model); + Assert.Equal(123.456m, person.PropertyWithDefaultValue); + Assert.True(bindingContext.ModelState.IsValid); + } + + [Fact] + public void SetProperty_PropertyIsReadOnly_DoesNothing() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + + // Assert + // If didn't throw, success! + } + + [Fact] + public void SetProperty_PropertyIsSettable_CallsSetter() + { + // Arrange + var model = new Person(); + var bindingContext = CreateContext(GetMetadataForObject(model)); + + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + var dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode); + var requiredValidator = bindingContext.ValidatorProviders + .SelectMany(v => v.GetValidators(propertyMetadata)) + .Where(v => v.IsRequired) + .FirstOrDefault(); + var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + validationNode.Validate(validationContext); + Assert.True(bindingContext.ModelState.IsValid); + Assert.Equal(new DateTime(2001, 1, 1), model.DateOfBirth); + } + + [Fact] + [ReplaceCulture] + public void SetProperty_PropertyIsSettable_SetterThrows_RecordsError() + { + // Arrange + var model = new Person + { + DateOfBirth = new DateTime(1900, 1, 1) + }; + var bindingContext = CreateContext(GetMetadataForObject(model)); + + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + var dtoResult = new ComplexModelDtoResult(new DateTime(1800, 1, 1), validationNode); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + + // Assert + Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine + + "Parameter name: value", + bindingContext.ModelState["foo"].Errors[0].Exception.Message); + } + + [Fact] + public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorNotPresent_RegistersValidationCallback() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); + var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + Assert.True(bindingContext.ModelState.IsValid); + validationNode.Validate(validationContext, bindingContext.ValidationNode); + Assert.False(bindingContext.ModelState.IsValid); + } + + [Fact] + public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorPresent_AddsModelError() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + bindingContext.ModelName = " foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + Assert.False(bindingContext.ModelState.IsValid); + Assert.Equal("Sample message", bindingContext.ModelState["foo.ValueTypeRequired"].Errors[0].ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void SetProperty_SettingNullableTypeToNull_RequiredValidatorNotPresent_PropertySetterThrows_AddsRequiredMessageString() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows())); + bindingContext.ModelName = "foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + Assert.False(bindingContext.ModelState.IsValid); + Assert.Equal(1, bindingContext.ModelState["foo.NameNoAttribute"].Errors.Count); + Assert.Equal("This is a different exception." + Environment.NewLine + + "Parameter name: value", + bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message); + } + + [Fact] + public void SetProperty_SettingNullableTypeToNull_RequiredValidatorPresent_PropertySetterThrows_AddsRequiredMessageString() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows())); + bindingContext.ModelName = "foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo.Name"); + var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + + // Assert + Assert.False(bindingContext.ModelState.IsValid); + Assert.Equal(1, bindingContext.ModelState["foo.Name"].Errors.Count); + Assert.Equal("This message comes from the [Required] attribute.", bindingContext.ModelState["foo.Name"].Errors[0].ErrorMessage); + } + + private static ModelBindingContext CreateContext(ModelMetadata metadata) + { + return new ModelBindingContext + { + ModelState = new ModelStateDictionary(), + ModelMetadata = metadata, + ModelName = "theModel", + ValidatorProviders = new IModelValidatorProvider[] + { + new DataAnnotationsModelValidatorProvider(), + new DataMemberModelValidatorProvider() + } + }; + } + + private static IModelValidator GetRequiredValidator(ModelBindingContext bindingContext, ModelMetadata propertyMetadata) + { + return bindingContext.ValidatorProviders + .SelectMany(v => v.GetValidators(propertyMetadata)) + .Where(v => v.IsRequired) + .FirstOrDefault(); + } + + private static ModelMetadata GetMetadataForCanUpdateProperty(string propertyName) + { + DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider(); + return metadataProvider.GetMetadataForProperty(null, typeof(MyModelTestingCanUpdateProperty), propertyName); + } + + private static ModelMetadata GetMetadataForObject(object o) + { + DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider(); + return metadataProvider.GetMetadataForType(() => o, o.GetType()); + } + + private static ModelMetadata GetMetadataForType(Type t) + { + DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider(); + return metadataProvider.GetMetadataForType(null, t); + } + + private class Person + { + private DateTime? _dateOfDeath; + + public DateTime DateOfBirth { get; set; } + + public DateTime? DateOfDeath + { + get { return _dateOfDeath; } + set + { + if (value < DateOfBirth) + { + throw new ArgumentOutOfRangeException("value", "Date of death can't be before date of birth."); + } + _dateOfDeath = value; + } + } + + [Required(ErrorMessage = "Sample message")] + public int ValueTypeRequired { get; set; } + + public string FirstName { get; set; } + public string LastName { get; set; } + public string NonUpdateableProperty { get; private set; } + + [DefaultValue(typeof(decimal), "123.456")] + public decimal PropertyWithDefaultValue { get; set; } + } + + private class PersonWithBindExclusion + { + [BindNever] + public DateTime DateOfBirth { get; set; } + + [BindNever] + public DateTime? DateOfDeath { get; set; } + + public string FirstName { get; set; } + public string LastName { get; set; } + public string NonUpdateableProperty { get; private set; } + } + + private class ModelWithRequired + { + public string Name { get; set; } + + [Required] + public int Age { get; set; } + + [Required] + public string City { get; set; } + } + + private class ModelWithBindRequired + { + public string Name { get; set; } + + [BindRequired] + public int Age { get; set; } + } + + [BindRequired] + private class ModelWithMixedBindingBehaviors + { + public string Required { get; set; } + + [BindNever] + public string Never { get; set; } + + [BindingBehavior(BindingBehavior.Optional)] + public string Optional { get; set; } + } + + private sealed class MyModelTestingCanUpdateProperty + { + public int ReadOnlyInt { get; private set; } + public string ReadOnlyString { get; private set; } + public string[] ReadOnlyArray { get; private set; } + public object ReadOnlyObject { get; private set; } + public string ReadWriteString { get; set; } + } + + private sealed class ModelWhosePropertySetterThrows + { + [Required(ErrorMessage = "This message comes from the [Required] attribute.")] + public string Name + { + get { return null; } + set { throw new ArgumentException("This is an exception.", "value"); } + } + + public string NameNoAttribute + { + get { return null; } + set { throw new ArgumentException("This is a different exception.", "value"); } + } + } + + public class TestableMutableObjectModelBinder : MutableObjectModelBinder + { + public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata) + { + return base.CanUpdateProperty(propertyMetadata); + } + + protected override bool CanUpdateProperty(ModelMetadata propertyMetadata) + { + return CanUpdatePropertyPublic(propertyMetadata); + } + + public virtual object CreateModelPublic(ModelBindingContext bindingContext) + { + return base.CreateModel(bindingContext); + } + + protected override object CreateModel(ModelBindingContext bindingContext) + { + return CreateModelPublic(bindingContext); + } + + public virtual void EnsureModelPublic(ModelBindingContext bindingContext) + { + base.EnsureModel(bindingContext); + } + + protected override void EnsureModel(ModelBindingContext bindingContext) + { + EnsureModelPublic(bindingContext); + } + + public virtual IEnumerable GetMetadataForPropertiesPublic(ModelBindingContext bindingContext) + { + return base.GetMetadataForProperties(bindingContext); + } + + protected override IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) + { + return GetMetadataForPropertiesPublic(bindingContext); + } + + public virtual void SetPropertyPublic(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, IModelValidator requiredValidator) + { + base.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); + } + + protected override void SetProperty(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, IModelValidator requiredValidator) + { + SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + } + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs index 5d437314dc..067dd92d0b 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorProviderTest.cs @@ -64,7 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var provider = new DataAnnotationsModelValidatorProvider(); var model = new ObservableModel(); var metadata = _metadataProvider.GetMetadataForProperty(() => model.TheProperty, typeof(ObservableModel), "TheProperty"); - var context = new ModelValidationContext(metadata, null, null, null); + var context = new ModelValidationContext(null, null, null, metadata, null); // Act var validators = provider.GetValidators(metadata).ToArray(); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs index 7e34a7b358..b4f7c08813 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs @@ -218,7 +218,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static ModelValidationContext CreateValidationContext(ModelMetadata metadata) { - return new ModelValidationContext(metadata, null, null, null); + return new ModelValidationContext(null, null, null, metadata, null); } class DerivedRequiredAttribute : RequiredAttribute diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs index 382edda2f6..a6ce9de632 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelValidationNodeTest.cs @@ -275,10 +275,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding new DataMemberModelValidatorProvider() }; - return new ModelValidationContext(metadata, + return new ModelValidationContext(new EmptyModelMetadataProvider(), + providers, new ModelStateDictionary(), - new EmptyModelMetadataProvider(), - providers); + metadata, + null); } private sealed class LoggingValidatableObject : IValidatableObject