// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if DNX451 using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Testing; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding { public class MutableObjectModelBinderTest { [Theory] [InlineData(true, true, "", "", false)] [InlineData(true, false, "", "", true)] [InlineData(false, true, "", "", false)] // !isTopLevelObject && isFirstChanceBinding cases are unexpected [InlineData(false, false, "", "", false)] [InlineData(true, true, "prefix", "", false)] [InlineData(true, false, "prefix", "", true)] [InlineData(false, true, "prefix", "", false)] [InlineData(false, false, "prefix", "", false)] [InlineData(true, true, "", "dummyModelName", false)] [InlineData(true, false, "", "dummyModelName", true)] [InlineData(false, true, "", "dummyModelName", false)] [InlineData(false, false, "", "dummyModelName", false)] [InlineData(true, true, "prefix", "dummyModelName", false)] [InlineData(true, false, "prefix", "dummyModelName", true)] [InlineData(false, true, "prefix", "dummyModelName", false)] [InlineData(false, false, "prefix", "dummyModelName", false)] public async Task CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndNotIsFirstChanceBinding( bool isTopLevelObject, bool isFirstChanceBinding, string binderModelName, string modelName, bool expectedCanCreate) { var mockValueProvider = new Mock(); mockValueProvider .Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); var metadataProvider = new TestModelMetadataProvider(); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { IsTopLevelObject = isTopLevelObject, IsFirstChanceBinding = isFirstChanceBinding, // Random type. ModelMetadata = metadataProvider.GetMetadataForType(typeof(Person)), ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ValueProvider = mockValueProvider.Object, MetadataProvider = metadataProvider, ValidatorProvider = Mock.Of(), }, // CanCreateModel() ignores the BinderModelName and ModelName properties. BinderModelName = binderModelName, ModelName = modelName, }, }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var canCreate = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.Equal(expectedCanCreate, canCreate); } [Fact] public async Task CanCreateModel_ReturnsFalse_IfNotIsTopLevelObjectAndModelIsMarkedWithBinderMetadata() { // Get the property metadata so that it is not a top level object. var modelMetadata = GetMetadataForType(typeof(Document)) .Properties .First(metadata => metadata.PropertyName == nameof(Document.SubDocument)); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = modelMetadata, OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), }, BindingSource = modelMetadata.BindingSource, BinderModelName = modelMetadata.BinderModelName, } }; var mutableBinder = new MutableObjectModelBinder(); // Act var canCreate = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.False(canCreate); } [Fact] public async Task CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndModelIsMarkedWithBinderMetadata() { var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { // Here the metadata represents a top level object. IsTopLevelObject = true, ModelMetadata = GetMetadataForType(typeof(Document)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), } } }; var mutableBinder = new MutableObjectModelBinder(); // Act var canCreate = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.True(canCreate); } [Fact] public async Task CanCreateModel_CreatesModel_IfTheModelIsBinderPoco() { var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(typeof(BinderMetadataPocoType)), ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), ValueProvider = mockValueProvider.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), }, // Setting it to empty ensures that model does not get created becasue of no model name. ModelName = "dummyModelName", }, }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var retModel = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.True(retModel); } [Theory] [InlineData(true)] [InlineData(false)] public async Task CanCreateModel_ReturnsTrue_IfNotIsTopLevelObject_BasedOnValueAvailability( bool valueAvailable) { // Arrange var mockValueProvider = new Mock(MockBehavior.Strict); mockValueProvider .Setup(provider => provider.ContainsPrefixAsync("SimpleContainer.Simple.Name")) .Returns(Task.FromResult(valueAvailable)); var typeMetadata = GetMetadataForType(typeof(SimpleContainer)); var modelMetadata = typeMetadata.Properties[nameof(SimpleContainer.Simple)]; var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = modelMetadata, ModelName = "SimpleContainer.Simple", OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), ValueProvider = mockValueProvider.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), }, ValueProvider = mockValueProvider.Object, }, PropertyMetadata = modelMetadata.Properties, }; var mutableBinder = new MutableObjectModelBinder(); // Act var result = await mutableBinder.CanCreateModel(bindingContext); // Assert // Result matches whether first Simple property can bind. Assert.Equal(valueAvailable, result); } [Fact] public async Task CanCreateModel_ReturnsFalse_IfNotIsTopLevelObjectAndModelHasNoProperties() { // Arrange var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { IsTopLevelObject = false, ModelMetadata = GetMetadataForType(typeof(PersonWithNoProperties)) } }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var canCreate = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.False(canCreate); } [Fact] public async Task CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndModelHasNoProperties() { // Arrange var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { IsTopLevelObject = true, ModelMetadata = GetMetadataForType(typeof(PersonWithNoProperties)) }, }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var retModel = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.True(retModel); } [Theory] [InlineData(typeof(TypeWithNoBinderMetadata), false)] [InlineData(typeof(TypeWithNoBinderMetadata), true)] [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)] [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)] [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)] [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)] public async Task CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue (Type modelType, bool valueProviderProvidesValue) { var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(valueProviderProvidesValue)); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(modelType), ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), ValueProvider = mockValueProvider.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), }, // Setting it to empty ensures that model does not get created becasue of no model name. ModelName = "dummyName" } }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var retModel = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.Equal(valueProviderProvidesValue, retModel); } [Theory] [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)] [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)] public async Task CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider( Type modelType, bool originalValueProviderProvidesValue) { var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); var mockOriginalValueProvider = new Mock(); mockOriginalValueProvider .Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(originalValueProviderProvidesValue)); mockOriginalValueProvider .Setup(o => o.Filter(It.IsAny())) .Returns(source => { if (source == BindingSource.Query) { return mockOriginalValueProvider.Object; } return null; }); var modelMetadata = GetMetadataForType(modelType); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = modelMetadata, ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ValueProvider = mockOriginalValueProvider.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of(), }, // Setting it to empty ensures that model does not get created becasue of no model name. ModelName = "dummyName", BindingSource = modelMetadata.BindingSource, BinderModelName = modelMetadata.BinderModelName } }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var retModel = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.Equal(originalValueProviderProvidesValue, retModel); } [Theory] [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)] [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)] [InlineData(typeof(TypeWithNoBinderMetadata), false)] [InlineData(typeof(TypeWithNoBinderMetadata), true)] public async Task CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(Type modelType, bool valueProviderProvidesValue) { var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(valueProviderProvidesValue)); var mockOriginalValueProvider = new Mock(); mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); var bindingContext = new MutableObjectBinderContext { ModelBindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(modelType), ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), ValueProvider = mockOriginalValueProvider.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), }, // Setting it to empty ensures that model does not get created becasue of no model name. ModelName = "dummyName" } }; var mutableBinder = new TestableMutableObjectModelBinder(); bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties( bindingContext.ModelBindingContext).ToArray(); // Act var retModel = await mutableBinder.CanCreateModel(bindingContext); // Assert Assert.Equal(valueProviderProvidesValue, retModel); } [Fact] public async Task BindModel_InitsInstance() { // Arrange var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(true)); var mockDtoBinder = new Mock(); var bindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(typeof(Person)), ModelName = "someName", ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ModelBinder = mockDtoBinder.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of() } }; mockDtoBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns((ModelBindingContext mbc) => { // just return the DTO unchanged return Task.FromResult(new ModelBindingResult(mbc.Model, mbc.ModelName, true)); }); var model = new Person(); var testableBinder = new Mock { CallBase = true }; testableBinder .Setup(o => o.EnsureModelPublic(bindingContext)) .Callback(c => c.Model = model) .Verifiable(); testableBinder .Setup(o => o.GetMetadataForProperties(bindingContext)) .Returns(new ModelMetadata[0]); // Act var retValue = await testableBinder.Object.BindModelAsync(bindingContext); // Assert Assert.NotNull(retValue); Assert.True(retValue.IsModelSet); var returnedPerson = Assert.IsType(retValue.Model); Assert.Same(model, returnedPerson); testableBinder.Verify(); } [Fact] public async Task BindModel_InitsInstance_IfIsTopLevelObjectAndNotIsFirstChanceBinding() { // Arrange var mockValueProvider = new Mock(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); var mockDtoBinder = new Mock(); var bindingContext = new ModelBindingContext { IsTopLevelObject = true, ModelMetadata = GetMetadataForType(typeof(Person)), ModelName = "", ValueProvider = mockValueProvider.Object, OperationBindingContext = new OperationBindingContext { ModelBinder = mockDtoBinder.Object, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of() } }; mockDtoBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns((ModelBindingContext mbc) => { // just return the DTO unchanged return Task.FromResult(new ModelBindingResult(mbc.Model, mbc.ModelName, true)); }); var model = new Person(); var testableBinder = new Mock { CallBase = true }; testableBinder .Setup(o => o.EnsureModelPublic(bindingContext)) .Callback(c => c.Model = model) .Verifiable(); testableBinder .Setup(o => o.GetMetadataForProperties(bindingContext)) .Returns(new ModelMetadata[0]); // Act var retValue = await testableBinder.Object.BindModelAsync(bindingContext); // Assert Assert.NotNull(retValue); Assert.True(retValue.IsModelSet); var returnedPerson = Assert.IsType(retValue.Model); Assert.Same(model, returnedPerson); testableBinder.Verify(); } [Theory] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyArray), false)] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyInt), false)] // read-only value type [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), true)] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlySimple), true)] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyString), false)] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadWriteString), true)] public void CanUpdateProperty_ReturnsExpectedValue(string propertyName, bool expected) { // Arrange var propertyMetadata = GetMetadataForCanUpdateProperty(propertyName); // Act var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata); // Assert Assert.Equal(expected, canUpdate); } [Theory] [InlineData(nameof(CollectionContainer.ReadOnlyArray), false)] [InlineData(nameof(CollectionContainer.ReadOnlyDictionary), true)] [InlineData(nameof(CollectionContainer.ReadOnlyList), true)] [InlineData(nameof(CollectionContainer.SettableArray), true)] [InlineData(nameof(CollectionContainer.SettableDictionary), true)] [InlineData(nameof(CollectionContainer.SettableList), true)] public void CanUpdateProperty_CollectionProperty_FalseOnlyForArray(string propertyName, bool expected) { // Arrange var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName); // Act var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(metadata); // Assert Assert.Equal(expected, 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 { Model = new Person(), ModelMetadata = GetMetadataForType(typeof(Person)) }; var testableBinder = new Mock { CallBase = true }; // Act var originalModel = bindingContext.Model; testableBinder.Object.EnsureModelPublic(bindingContext); // Assert Assert.Same(originalModel, bindingContext.Model); 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 var originalModel = bindingContext.Model; testableBinder.Object.EnsureModelPublic(bindingContext); var 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)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider() } }; var testableBinder = new TestableMutableObjectModelBinder(); // Act var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext); var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray(); // Assert Assert.Equal(expectedPropertyNames, returnedPropertyNames); } [Fact] public void GetMetadataForProperties_WithoutBindAttribute() { // Arrange var expectedPropertyNames = new[] { nameof(Person.DateOfBirth), nameof(Person.DateOfDeath), nameof(Person.ValueTypeRequired), nameof(Person.ValueTypeRequiredWithDefaultValue), nameof(Person.FirstName), nameof(Person.LastName), nameof(Person.PropertyWithDefaultValue), nameof(Person.PropertyWithInitializedValue), nameof(Person.PropertyWithInitializedValueAndDefault), }; var bindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(typeof(Person)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider() }, }; var testableBinder = new TestableMutableObjectModelBinder(); // Act var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext); var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray(); // Assert Assert.Equal(expectedPropertyNames, returnedPropertyNames); } [Fact] public void GetMetadataForProperties_DoesNotReturn_ExcludedProperties() { // Arrange var expectedPropertyNames = new[] { "IncludedByDefault1", "IncludedByDefault2" }; var bindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(typeof(TypeWithExcludedPropertiesUsingBindAttribute)), OperationBindingContext = new OperationBindingContext { HttpContext = new DefaultHttpContext { RequestServices = CreateServices() }, ValidatorProvider = Mock.Of(), MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), } }; var testableBinder = new TestableMutableObjectModelBinder(); // Act var propertyMetadatas = testableBinder.GetMetadataForProperties(bindingContext); var returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray(); // Assert Assert.Equal(expectedPropertyNames, returnedPropertyNames); } [Fact] public void GetMetadataForProperties_ReturnsOnlyIncludedProperties_UsingBindAttributeInclude() { // Arrange var expectedPropertyNames = new[] { "IncludedExplicitly1", "IncludedExplicitly2" }; var bindingContext = new ModelBindingContext { ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), } }; var testableBinder = new TestableMutableObjectModelBinder(); // Act var propertyMetadatas = testableBinder.GetMetadataForProperties(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 = GetMetadataForType(typeof(ModelWithMixedBindingBehaviors)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of() } }; // Act var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext); // Assert Assert.Equal(new[] { "Required" }, validationInfo.RequiredProperties); Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties); } [Fact] public void GetPropertyValidationInfo_WithIndexerProperties_Succeeds() { // Arrange var bindingContext = new ModelBindingContext { // Any type, even an otherwise-simple POCO with an indexer property, would do here. ModelMetadata = GetMetadataForType(typeof(List)), OperationBindingContext = new OperationBindingContext { ValidatorProvider = Mock.Of(), }, }; // Act var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext); // Assert Assert.Equal(Enumerable.Empty(), validationInfo.RequiredProperties); Assert.Equal(Enumerable.Empty(), validationInfo.SkipProperties); } [Fact] [ReplaceCulture] public void ProcessDto_BindRequiredFieldMissing_RaisesModelError() { // Arrange var model = new ModelWithBindRequired { Name = "original value", Age = -20 }; var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = new ModelBindingContext { Model = model, ModelMetadata = containerMetadata, ModelName = "theModel", OperationBindingContext = new OperationBindingContext { MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of() } }; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); dto.Results[nameProperty] = new ModelBindingResult( "John Doe", isModelSet: true, key: ""); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert var modelStateDictionary = bindingContext.ModelState; Assert.False(modelStateDictionary.IsValid); Assert.Single(modelStateDictionary); // Check Age error. ModelState modelState; Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); var modelError = Assert.Single(modelState.Errors); Assert.Null(modelError.Exception); Assert.NotNull(modelError.ErrorMessage); Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage); } [Fact] [ReplaceCulture] public void ProcessDto_DataMemberIsRequiredFieldMissing_RaisesModelError() { // Arrange var model = new ModelWithDataMemberIsRequired { Name = "original value", Age = -20 }; var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = new ModelBindingContext { Model = model, ModelMetadata = containerMetadata, ModelName = "theModel", OperationBindingContext = new OperationBindingContext { MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of() } }; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); dto.Results[nameProperty] = new ModelBindingResult( "John Doe", isModelSet: true, key: ""); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert var modelStateDictionary = bindingContext.ModelState; Assert.False(modelStateDictionary.IsValid); Assert.Single(modelStateDictionary); // Check Age error. ModelState modelState; Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); var modelError = Assert.Single(modelState.Errors); Assert.Null(modelError.Exception); Assert.NotNull(modelError.ErrorMessage); Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage); } [Fact] [ReplaceCulture] public void ProcessDto_ValueTypePropertyWithBindRequired_SetToNull_CapturesException() { // Arrange var model = new ModelWithBindRequired { Name = "original value", Age = -20 }; var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = new ModelBindingContext() { Model = model, ModelMetadata = containerMetadata, ModelName = "theModel", ModelState = new ModelStateDictionary(), OperationBindingContext = new OperationBindingContext { MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = Mock.Of() } }; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); var propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); dto.Results[propertyMetadata] = new ModelBindingResult( "John Doe", isModelSet: true, key: "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 ModelBindingResult( null, isModelSet: true, key: "theModel.Age"); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // 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(ModelValidationState.Invalid, modelState.ValidationState); var modelError = Assert.Single(modelState.Errors); Assert.Equal(string.Empty, modelError.ErrorMessage); Assert.IsType(modelError.Exception); } [Fact] [ReplaceCulture] public void ProcessDto_MissingDataForRequiredFields_NoErrors() { // Arrange var model = new ModelWithRequired(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); // 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(); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert var modelStateDictionary = bindingContext.ModelState; Assert.True(modelStateDictionary.IsValid); Assert.Empty(modelStateDictionary); } [Fact] [ReplaceCulture] public void ProcessDto_ValueTypeProperty_WithRequiredAttribute_SetToNull_NoError() { // Arrange var model = new ModelWithRequired(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); 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 ModelBindingResult( 23, isModelSet: true, key: "theModel.Age"); propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "City"); dto.Results[propertyMetadata] = new ModelBindingResult( null, isModelSet: true, key: "theModel.City"); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert var modelStateDictionary = bindingContext.ModelState; Assert.True(modelStateDictionary.IsValid); Assert.Empty(modelStateDictionary); } [Fact] public void ProcessDto_PropertyWithRequiredAttribute_NoPropertiesSet_NoError() { // Arrange var model = new Person(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); // Set no properties though ValueTypeRequired (a non-Nullable struct) property is required. var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert var modelStateDictionary = bindingContext.ModelState; Assert.True(modelStateDictionary.IsValid); } [Fact] public void ProcessDto_ValueTypeProperty_TriesToSetNullModel_CapturesException() { // Arrange var model = new Person(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var modelStateDictionary = bindingContext.ModelState; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); // The [DefaultValue] on ValueTypeRequiredWithDefaultValue is ignored by model binding. var expectedValue = 0; // Make ValueTypeRequired invalid. var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); dto.Results[propertyMetadata] = new ModelBindingResult( null, isModelSet: true, key: "theModel." + nameof(Person.ValueTypeRequired)); // Make ValueTypeRequiredWithDefaultValue invalid propertyMetadata = dto.PropertyMetadata .Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue)); dto.Results[propertyMetadata] = new ModelBindingResult( model: null, isModelSet: true, key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.False(modelStateDictionary.IsValid); // Check ValueTypeRequired error. var modelStateEntry = Assert.Single( modelStateDictionary, entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequired)); Assert.Equal("theModel." + nameof(Person.ValueTypeRequired), modelStateEntry.Key); var modelState = modelStateEntry.Value; Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState); var error = Assert.Single(modelState.Errors); Assert.Equal(string.Empty, error.ErrorMessage); Assert.IsType(error.Exception); // Check ValueTypeRequiredWithDefaultValue error. modelStateEntry = Assert.Single( modelStateDictionary, entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); Assert.Equal("theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), modelStateEntry.Key); modelState = modelStateEntry.Value; Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState); error = Assert.Single(modelState.Errors); Assert.Equal(string.Empty, error.ErrorMessage); Assert.IsType(error.Exception); Assert.Equal(0, model.ValueTypeRequired); Assert.Equal(expectedValue, model.ValueTypeRequiredWithDefaultValue); } [Fact] public void ProcessDto_ValueTypeProperty_NoValue_NoError() { // Arrange var model = new Person(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var modelStateDictionary = bindingContext.ModelState; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); // Make ValueTypeRequired invalid. var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); dto.Results[propertyMetadata] = new ModelBindingResult( null, isModelSet: false, key: "theModel." + nameof(Person.ValueTypeRequired)); // Make ValueTypeRequiredWithDefaultValue invalid propertyMetadata = dto.PropertyMetadata .Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue)); dto.Results[propertyMetadata] = new ModelBindingResult( model: null, isModelSet: false, key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.True(modelStateDictionary.IsValid); Assert.Empty(modelStateDictionary); } [Fact] public void ProcessDto_ProvideRequiredFields_Success() { // Arrange var model = new Person(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var modelStateDictionary = bindingContext.ModelState; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); // Make ValueTypeRequired valid. var propertyMetadata = dto.PropertyMetadata .Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); dto.Results[propertyMetadata] = new ModelBindingResult( 41, isModelSet: true, key: "theModel." + nameof(Person.ValueTypeRequired)); // Make ValueTypeRequiredWithDefaultValue valid. propertyMetadata = dto.PropertyMetadata .Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue)); dto.Results[propertyMetadata] = new ModelBindingResult( model: 57, isModelSet: true, key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); // Also remind ProcessDto about PropertyWithDefaultValue -- as ComplexModelDtoModelBinder would. propertyMetadata = dto.PropertyMetadata .Single(p => p.PropertyName == nameof(Person.PropertyWithDefaultValue)); dto.Results[propertyMetadata] = new ModelBindingResult( model: null, isModelSet: false, key: "theModel." + nameof(Person.PropertyWithDefaultValue)); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.True(modelStateDictionary.IsValid); Assert.Empty(modelStateDictionary); // Model gets provided values. Assert.Equal(41, model.ValueTypeRequired); Assert.Equal(57, model.ValueTypeRequiredWithDefaultValue); Assert.Equal(0m, model.PropertyWithDefaultValue); // [DefaultValue] has no effect } // [Required] cannot provide a custom validation for [BindRequired] errors. [Fact] public void ProcessDto_ValueTypePropertyWithBindRequired_RequiredValidatorIgnored() { // Arrange var model = new ModelWithBindRequiredAndRequiredAttribute(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var modelStateDictionary = bindingContext.ModelState; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); // Make ValueTypeProperty not have a value. var propertyMetadata = containerMetadata .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)]; dto.Results[propertyMetadata] = new ModelBindingResult( null, isModelSet: false, key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)); // Make ReferenceTypeProperty have a value. propertyMetadata = containerMetadata .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)]; dto.Results[propertyMetadata] = new ModelBindingResult( model: "value", isModelSet: true, key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.False(modelStateDictionary.IsValid); var entry = Assert.Single( modelStateDictionary, kvp => kvp.Key == "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)) .Value; var error = Assert.Single(entry.Errors); Assert.Null(error.Exception); Assert.Equal("A value for the 'ValueTypeProperty' property was not provided.", error.ErrorMessage); // Model gets provided values. Assert.Equal(0, model.ValueTypeProperty); Assert.Equal("value", model.ReferenceTypeProperty); } // [Required] cannot provide a custom validation for [BindRequired] errors. [Fact] public void ProcessDto_ReferenceTypePropertyWithBindRequired_RequiredValidatorIgnored() { // Arrange var model = new ModelWithBindRequiredAndRequiredAttribute(); var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var modelStateDictionary = bindingContext.ModelState; var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var testableBinder = new TestableMutableObjectModelBinder(); // Make ValueTypeProperty have a value. var propertyMetadata = containerMetadata .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)]; dto.Results[propertyMetadata] = new ModelBindingResult( 17, isModelSet: true, key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)); // Make ReferenceTypeProperty not have a value. propertyMetadata = containerMetadata .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)]; dto.Results[propertyMetadata] = new ModelBindingResult( model: null, isModelSet: false, key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.False(modelStateDictionary.IsValid); var entry = Assert.Single( modelStateDictionary, kvp => kvp.Key == "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)) .Value; var error = Assert.Single(entry.Errors); Assert.Null(error.Exception); Assert.Equal("A value for the 'ReferenceTypeProperty' property was not provided.", error.ErrorMessage); // Model gets provided values. Assert.Equal(17, model.ValueTypeProperty); Assert.Null(model.ReferenceTypeProperty); } [Fact] public void ProcessDto_Success() { // Arrange var dob = new DateTime(2001, 1, 1); var model = new PersonWithBindExclusion { DateOfBirth = dob }; var containerMetadata = GetMetadataForType(model.GetType()); var bindingContext = CreateContext(containerMetadata, model); var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName"); dto.Results[firstNameProperty] = new ModelBindingResult( "John", isModelSet: true, key: ""); var lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName"); dto.Results[lastNameProperty] = new ModelBindingResult( "Doe", isModelSet: true, key: ""); var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth"); dto.Results[dobProperty] = null; var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.ProcessDto(bindingContext, dto, modelValidationNode); // Assert Assert.Equal("John", model.FirstName); Assert.Equal("Doe", model.LastName); Assert.Equal(dob, model.DateOfBirth); Assert.True(bindingContext.ModelState.IsValid); // Ensure that we add child nodes for all the nodes which have a result (irrespective of if they // are bound or not). Assert.Equal(2, modelValidationNode.ChildNodes.Count()); var validationNode = modelValidationNode.ChildNodes[0]; Assert.Equal("", validationNode.Key); Assert.Equal("John", validationNode.Model); validationNode = modelValidationNode.ChildNodes[1]; Assert.Equal("", validationNode.Key); Assert.Equal("Doe", validationNode.Model); } [Fact] public void SetProperty_PropertyHasDefaultValue_DefaultValueAttributeDoesNothing() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["PropertyWithDefaultValue"]; var dtoResult = new ModelBindingResult( model: null, isModelSet: false, key: "foo"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert var person = Assert.IsType(bindingContext.Model); Assert.Equal(0m, person.PropertyWithDefaultValue); Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void SetProperty_PropertyIsPreinitialized_NoValue_DoesNothing() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["PropertyWithInitializedValue"]; // This value won't be used because IsModelBound = false. var dtoResult = new ModelBindingResult( model: "bad-value", isModelSet: false, key: "foo"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert var person = Assert.IsType(bindingContext.Model); Assert.Equal("preinitialized", person.PropertyWithInitializedValue); Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void SetProperty_PropertyIsPreinitialized_DefaultValueAttributeDoesNothing() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["PropertyWithInitializedValueAndDefault"]; // This value won't be used because IsModelBound = false. var dtoResult = new ModelBindingResult( model: "bad-value", isModelSet: false, key: "foo"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert var person = Assert.IsType(bindingContext.Model); Assert.Equal("preinitialized", person.PropertyWithInitializedValueAndDefault); Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void SetProperty_PropertyIsReadOnly_DoesNothing() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["NonUpdateableProperty"]; var dtoResult = new ModelBindingResult( model: null, isModelSet: false, key: "foo"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert // If didn't throw, success! } // Property name, property accessor public static TheoryData> MyCanUpdateButCannotSetPropertyData { get { return new TheoryData> { { nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), model => ((Simple)((MyModelTestingCanUpdateProperty)model).ReadOnlyObject).Name }, { nameof(MyModelTestingCanUpdateProperty.ReadOnlySimple), model => ((MyModelTestingCanUpdateProperty)model).ReadOnlySimple.Name }, }; } } // Reviewers: Is this inconsistency with CanUpdateProperty() an issue we should be tracking? [Theory] [MemberData(nameof(MyCanUpdateButCannotSetPropertyData))] public void SetProperty_ValueProvidedAndCanUpdatePropertyTrue_DoesNothing( string propertyName, Func propertAccessor) { // Arrange var model = new MyModelTestingCanUpdateProperty(); var type = model.GetType(); var bindingContext = CreateContext(GetMetadataForType(type), model); var modelState = bindingContext.ModelState; var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(type, model); var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName]; var dtoResult = new ModelBindingResult( model: new Simple { Name = "Hanna" }, isModelSet: true, key: propertyName); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert Assert.Equal("Joe", propertAccessor(model)); Assert.True(modelState.IsValid); Assert.Empty(modelState); } // Property name, property accessor, collection. public static TheoryData, object> CollectionPropertyData { get { return new TheoryData, object> { { nameof(CollectionContainer.ReadOnlyDictionary), model => ((CollectionContainer)model).ReadOnlyDictionary, new Dictionary { { 1, "one" }, { 2, "two" }, { 3, "three" }, } }, { nameof(CollectionContainer.ReadOnlyList), model => ((CollectionContainer)model).ReadOnlyList, new List { 1, 2, 3, 4 } }, { nameof(CollectionContainer.SettableArray), model => ((CollectionContainer)model).SettableArray, new int[] { 1, 2, 3, 4 } }, { nameof(CollectionContainer.SettableDictionary), model => ((CollectionContainer)model).SettableDictionary, new Dictionary { { 1, "one" }, { 2, "two" }, { 3, "three" }, } }, { nameof(CollectionContainer.SettableList), model => ((CollectionContainer)model).SettableList, new List { 1, 2, 3, 4 } }, }; } } [Theory] [MemberData(nameof(CollectionPropertyData))] public void SetProperty_CollectionProperty_UpdatesModel( string propertyName, Func propertyAccessor, object collection) { // Arrange var model = new CollectionContainer(); var type = model.GetType(); var bindingContext = CreateContext(GetMetadataForType(type), model); var modelState = bindingContext.ModelState; var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(type, model); var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName]; var dtoResult = new ModelBindingResult(model: collection, isModelSet: true, key: propertyName); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert Assert.Equal(collection, propertyAccessor(model)); Assert.True(modelState.IsValid); Assert.Empty(modelState); } [Fact] public void SetProperty_PropertyIsSettable_CallsSetter() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["DateOfBirth"]; var dtoResult = new ModelBindingResult( new DateTime(2001, 1, 1), key: "foo", isModelSet: true); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert 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(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["DateOfDeath"]; var dtoResult = new ModelBindingResult( new DateTime(1800, 1, 1), isModelSet: true, key: "foo"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // 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); } // This can only really be done by writing an invalid model binder and returning 'isModelSet: true' // with a null model for a value type. [Fact] public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException() { // Arrange var model = new Person(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["DateOfBirth"]; var dtoResult = new ModelBindingResult( model: null, isModelSet: true, key: "foo.DateOfBirth"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // Assert Assert.False(bindingContext.ModelState.IsValid); var entry = Assert.Single(bindingContext.ModelState, kvp => kvp.Key == "foo.DateOfBirth").Value; var error = Assert.Single(entry.Errors); Assert.Equal(string.Empty, error.ErrorMessage); Assert.IsType(error.Exception); } [Fact] [ReplaceCulture] public void SetProperty_PropertySetterThrows_CapturesException() { // Arrange var model = new ModelWhosePropertySetterThrows(); var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model); bindingContext.ModelName = "foo"; var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(ModelWhosePropertySetterThrows), model); var propertyMetadata = bindingContext.ModelMetadata.Properties["NameNoAttribute"]; var dtoResult = new ModelBindingResult( model: null, isModelSet: true, key: "foo.NameNoAttribute"); var testableBinder = new TestableMutableObjectModelBinder(); // Act testableBinder.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); // 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); } private static ModelBindingContext CreateContext(ModelMetadata metadata, object model) { return new ModelBindingContext { Model = model, ModelState = new ModelStateDictionary(), ModelMetadata = metadata, ModelName = "theModel", OperationBindingContext = new OperationBindingContext { MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), ValidatorProvider = TestModelValidatorProvider.CreateDefaultProvider(), } }; } private static ModelMetadata GetMetadataForCanUpdateProperty(string propertyName) { var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); return metadataProvider.GetMetadataForProperty(typeof(MyModelTestingCanUpdateProperty), propertyName); } private static ModelMetadata GetMetadataForType(Type t) { var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); return metadataProvider.GetMetadataForType(t); } private class EmptyModel { } private class Person { private DateTime? _dateOfDeath; public DateTime DateOfBirth { get; set; } public DateTime? DateOfDeath { get { return _dateOfDeath; } set { if (value < DateOfBirth) { throw new ArgumentOutOfRangeException(nameof(value), "Date of death can't be before date of birth."); } _dateOfDeath = value; } } [Required(ErrorMessage = "Sample message")] public int ValueTypeRequired { get; set; } [Required(ErrorMessage = "Another sample message")] [DefaultValue(42)] public int ValueTypeRequiredWithDefaultValue { 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; } public string PropertyWithInitializedValue { get; set; } = "preinitialized"; [DefaultValue("default")] public string PropertyWithInitializedValueAndDefault { get; set; } = "preinitialized"; } private class PersonWithNoProperties { public string name; } 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; } } [DataContract] private class ModelWithDataMemberIsRequired { public string Name { get; set; } [DataMember(IsRequired = true)] 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; } } [BindRequired] private class ModelWithBindRequiredAndRequiredAttribute { [Range(5, 20)] [Required(ErrorMessage = "Custom Message {0}")] public int ValueTypeProperty { get; set; } [StringLength(25)] [Required(ErrorMessage = "Custom Message {0}")] public string ReferenceTypeProperty { 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; } = new Simple { Name = "Joe" }; public string ReadWriteString { get; set; } public Simple ReadOnlySimple { get; } = new Simple { Name = "Joe" }; } 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"); } } } private class TypeWithNoBinderMetadata { public int UnMarkedProperty { get; set; } } private class BinderMetadataPocoType { [NonValueBinderMetadata] public string MarkedWithABinderMetadata { get; set; } } // Not a Metadata poco because there is a property with value binder Metadata. private class TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata { [NonValueBinderMetadata] public string MarkedWithABinderMetadata { get; set; } [ValueBinderMetadata] public string MarkedWithAValueBinderMetadata { get; set; } } // not a Metadata poco because there is an unmarked property. private class TypeWithUnmarkedAndBinderMetadataMarkedProperties { public int UnmarkedProperty { get; set; } [NonValueBinderMetadata] public string MarkedWithABinderMetadata { get; set; } } [Bind(new[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })] private class TypeWithIncludedPropertiesUsingBindAttribute { public int ExcludedByDefault1 { get; set; } public int ExcludedByDefault2 { get; set; } public int IncludedExplicitly1 { get; set; } public int IncludedExplicitly2 { get; set; } } [Bind(typeof(ExcludedProvider))] private class TypeWithExcludedPropertiesUsingBindAttribute { public int Excluded1 { get; set; } public int Excluded2 { get; set; } public int IncludedByDefault1 { get; set; } public int IncludedByDefault2 { get; set; } } private class Document { [NonValueBinderMetadata] public string Version { get; set; } [NonValueBinderMetadata] public Document SubDocument { get; set; } } private class NonValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Body; } } } private class ValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Query; } } } private class ExcludedProvider : IPropertyBindingPredicateProvider { public Func PropertyFilter { get { return (context, propertyName) => !string.Equals("Excluded1", propertyName, StringComparison.OrdinalIgnoreCase) && !string.Equals("Excluded2", propertyName, StringComparison.OrdinalIgnoreCase); } } } private class SimpleContainer { public Simple Simple { get; set; } } private class Simple { public string Name { get; set; } } private class CollectionContainer { public int[] ReadOnlyArray { get; } = new int[4]; // Read-only collections get added values. public IDictionary ReadOnlyDictionary { get; } = new Dictionary(); public IList ReadOnlyList { get; } = new List(); // Settable values are overwritten. public int[] SettableArray { get; set; } = new int[] { 0, 1 }; public IDictionary SettableDictionary { get; set; } = new Dictionary { { 0, "zero" }, { 25, "twenty-five" }, }; public IList SettableList { get; set; } = new List { 3, 9, 0 }; } private IServiceProvider CreateServices() { var services = new Mock(MockBehavior.Strict); return services.Object; } 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 new IEnumerable GetMetadataForProperties(ModelBindingContext bindingContext) { return base.GetMetadataForProperties(bindingContext); } public new void SetProperty( ModelBindingContext bindingContext, ModelExplorer modelExplorer, ModelMetadata propertyMetadata, ModelBindingResult dtoResult) { base.SetProperty( bindingContext, modelExplorer, propertyMetadata, dtoResult); } } } } #endif