File: ModelBinding\Binders\ComplexTypeModelBinderTest.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
#pragma warning disable CS0618 // Type or member is obsolete
public class ComplexTypeModelBinderTest
{
    private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
 
    [Theory]
    [InlineData(true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
    [InlineData(false, ComplexTypeModelBinder.NoDataAvailable)]
    public void CanCreateModel_ReturnsTrue_IfIsTopLevelObject(bool isTopLevelObject, int expectedCanCreate)
    {
        var bindingContext = CreateContext(GetMetadataForType(typeof(Person)));
        bindingContext.IsTopLevelObject = isTopLevelObject;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(expectedCanCreate, canCreate);
    }
 
    [Fact]
    public void CanCreateModel_ReturnsFalse_IfNotIsTopLevelObjectAndModelIsMarkedWithBinderMetadata()
    {
        var modelMetadata = GetMetadataForProperty(typeof(Document), nameof(Document.SubDocument));
 
        var bindingContext = CreateContext(modelMetadata);
        bindingContext.IsTopLevelObject = false;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(ComplexTypeModelBinder.NoDataAvailable, canCreate);
    }
 
    [Fact]
    public void CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndModelIsMarkedWithBinderMetadata()
    {
        var bindingContext = CreateContext(GetMetadataForType(typeof(Document)));
        bindingContext.IsTopLevelObject = true;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(ComplexTypeModelBinder.ValueProviderDataAvailable, canCreate);
    }
 
    [Theory]
    [InlineData(ComplexTypeModelBinder.ValueProviderDataAvailable)]
    [InlineData(ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
    public void CanCreateModel_CreatesModel_WithAllGreedyProperties(int expectedCanCreate)
    {
        var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties)));
        bindingContext.IsTopLevelObject = expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(expectedCanCreate, canCreate);
    }
 
    [Theory]
    [InlineData(ComplexTypeModelBinder.ValueProviderDataAvailable)]
    [InlineData(ComplexTypeModelBinder.NoDataAvailable)]
    public void CanCreateModel_ReturnsTrue_IfNotIsTopLevelObject_BasedOnValueAvailability(int valueAvailable)
    {
        // Arrange
        var valueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
        valueProvider
            .Setup(provider => provider.ContainsPrefix("SimpleContainer.Simple.Name"))
            .Returns(valueAvailable == ComplexTypeModelBinder.ValueProviderDataAvailable);
 
        var modelMetadata = GetMetadataForProperty(typeof(SimpleContainer), nameof(SimpleContainer.Simple));
        var bindingContext = CreateContext(modelMetadata);
        bindingContext.IsTopLevelObject = false;
        bindingContext.ModelName = "SimpleContainer.Simple";
        bindingContext.ValueProvider = valueProvider.Object;
        bindingContext.OriginalValueProvider = valueProvider.Object;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        // Result matches whether first Simple property can bind.
        Assert.Equal(valueAvailable, canCreate);
    }
 
    [Fact]
    public void CanCreateModel_ReturnsFalse_IfNotIsTopLevelObjectAndModelHasNoProperties()
    {
        // Arrange
        var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
        bindingContext.IsTopLevelObject = false;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(ComplexTypeModelBinder.NoDataAvailable, canCreate);
    }
 
    [Fact]
    public void CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndModelHasNoProperties()
    {
        // Arrange
        var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithNoProperties)));
        bindingContext.IsTopLevelObject = true;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(ComplexTypeModelBinder.ValueProviderDataAvailable, canCreate);
    }
 
    [Theory]
    [InlineData(typeof(TypeWithNoBinderMetadata), ComplexTypeModelBinder.NoDataAvailable)]
    [InlineData(typeof(TypeWithNoBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
    public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue(
        Type modelType,
        int valueProviderProvidesValue)
    {
        var valueProvider = new Mock<IValueProvider>();
        valueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(valueProviderProvidesValue == ComplexTypeModelBinder.ValueProviderDataAvailable);
 
        var bindingContext = CreateContext(GetMetadataForType(modelType));
        bindingContext.IsTopLevelObject = false;
        bindingContext.ValueProvider = valueProvider.Object;
        bindingContext.OriginalValueProvider = valueProvider.Object;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(valueProviderProvidesValue, canCreate);
    }
 
    [Theory]
    [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
    [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
    [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
    [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexTypeModelBinder.ValueProviderDataAvailable)]
    public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfPropertyHasGreedyBindingSource(
        Type modelType,
        int expectedCanCreate)
    {
        var valueProvider = new Mock<IValueProvider>();
        valueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable);
 
        var bindingContext = CreateContext(GetMetadataForType(modelType));
        bindingContext.IsTopLevelObject = false;
        bindingContext.ValueProvider = valueProvider.Object;
        bindingContext.OriginalValueProvider = valueProvider.Object;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(expectedCanCreate, canCreate);
    }
 
    [Theory]
    [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
    [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
    public void CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider(
        Type modelType,
        int expectedCanCreate)
    {
        var valueProvider = new Mock<IValueProvider>();
        valueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(false);
 
        var originalValueProvider = new Mock<IBindingSourceValueProvider>();
        originalValueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable);
 
        originalValueProvider
            .Setup(o => o.Filter(It.IsAny<BindingSource>()))
            .Returns<BindingSource>(source => source == BindingSource.Query ? originalValueProvider.Object : null);
 
        var bindingContext = CreateContext(GetMetadataForType(modelType));
        bindingContext.IsTopLevelObject = false;
        bindingContext.ValueProvider = valueProvider.Object;
        bindingContext.OriginalValueProvider = originalValueProvider.Object;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(expectedCanCreate, canCreate);
    }
 
    [Theory]
    [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false, ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
    [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
    [InlineData(typeof(TypeWithNoBinderMetadata), false, ComplexTypeModelBinder.NoDataAvailable)]
    [InlineData(typeof(TypeWithNoBinderMetadata), true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
    public void CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(
        Type modelType,
        bool valueProviderProvidesValue,
        int expectedCanCreate)
    {
        var valueProvider = new Mock<IValueProvider>();
        valueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(valueProviderProvidesValue);
 
        var originalValueProvider = new Mock<IValueProvider>();
        originalValueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(false);
 
        var bindingContext = CreateContext(GetMetadataForType(modelType));
        bindingContext.IsTopLevelObject = false;
        bindingContext.ValueProvider = valueProvider.Object;
        bindingContext.OriginalValueProvider = originalValueProvider.Object;
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var canCreate = binder.CanCreateModel(bindingContext);
 
        // Assert
        Assert.Equal(expectedCanCreate, canCreate);
    }
 
    private IActionResult ActionWithComplexParameter(Person parameter) => null;
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    public async Task BindModelAsync_CreatesModel_IfIsTopLevelObject(
        bool allowValidatingTopLevelNodes,
        bool isBindingRequired)
    {
        // Arrange
        var expectedErrorCount = isBindingRequired ? 1 : 0;
        var mockValueProvider = new Mock<IValueProvider>();
        mockValueProvider
            .Setup(o => o.ContainsPrefix(It.IsAny<string>()))
            .Returns(false);
 
        // Mock binder fails to bind all properties.
        var mockBinder = new StubModelBinder();
 
        var parameter = typeof(ComplexTypeModelBinderTest)
            .GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
            .GetParameters()[0];
        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider
            .ForParameter(parameter)
            .BindingDetails(b => b.IsBindingRequired = isBindingRequired);
        var metadata = metadataProvider.GetMetadataForParameter(parameter);
        var bindingContext = new DefaultModelBindingContext
        {
            IsTopLevelObject = true,
            ModelMetadata = metadata,
            ModelName = string.Empty,
            ValueProvider = mockValueProvider.Object,
            ModelState = new ModelStateDictionary(),
        };
 
        var model = new Person();
 
        var testableBinder = new Mock<TestableComplexTypeModelBinder>(allowValidatingTopLevelNodes)
        {
            CallBase = true
        };
        testableBinder
            .Setup(o => o.CreateModelPublic(bindingContext))
            .Returns(model)
            .Verifiable();
        testableBinder
            .Setup(o => o.CanBindPropertyPublic(bindingContext, It.IsAny<ModelMetadata>()))
            .Returns(false);
 
        // Act
        await testableBinder.Object.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.Equal(expectedErrorCount, bindingContext.ModelState.ErrorCount);
 
        var returnedPerson = Assert.IsType<Person>(bindingContext.Result.Model);
        Assert.Same(model, returnedPerson);
        testableBinder.Verify();
    }
 
    [Fact]
    public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoData()
    {
        // Arrange
        var parameter = typeof(ComplexTypeModelBinderTest)
            .GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
            .GetParameters()[0];
        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider
            .ForParameter(parameter)
            .BindingDetails(b => b.IsBindingRequired = true);
        var metadata = metadataProvider.GetMetadataForParameter(parameter);
        var bindingContext = new DefaultModelBindingContext
        {
            IsTopLevelObject = true,
            FieldName = "fieldName",
            ModelMetadata = metadata,
            ModelName = string.Empty,
            ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
            ModelState = new ModelStateDictionary(),
        };
 
        // Mock binder fails to bind all properties.
        var innerBinder = new StubModelBinder();
        var binders = new Dictionary<ModelMetadata, IModelBinder>();
        foreach (var property in metadataProvider.GetMetadataForProperties(typeof(Person)))
        {
            binders.Add(property, innerBinder);
        }
 
        var binder = new ComplexTypeModelBinder(
            binders,
            NullLoggerFactory.Instance,
            allowValidatingTopLevelNodes: true);
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.IsType<Person>(bindingContext.Result.Model);
 
        var keyValuePair = Assert.Single(bindingContext.ModelState);
        Assert.Equal(string.Empty, keyValuePair.Key);
        var error = Assert.Single(keyValuePair.Value.Errors);
        Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
    }
 
    private IActionResult ActionWithNoSettablePropertiesParameter(PersonWithNoProperties parameter) => null;
 
    [Fact]
    public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoSettableProperties()
    {
        // Arrange
        var parameter = typeof(ComplexTypeModelBinderTest)
            .GetMethod(
                nameof(ActionWithNoSettablePropertiesParameter),
                BindingFlags.Instance | BindingFlags.NonPublic)
            .GetParameters()[0];
        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider
            .ForParameter(parameter)
            .BindingDetails(b => b.IsBindingRequired = true);
        var metadata = metadataProvider.GetMetadataForParameter(parameter);
        var bindingContext = new DefaultModelBindingContext
        {
            IsTopLevelObject = true,
            FieldName = "fieldName",
            ModelMetadata = metadata,
            ModelName = string.Empty,
            ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
            ModelState = new ModelStateDictionary(),
        };
 
        var binder = new ComplexTypeModelBinder(
            new Dictionary<ModelMetadata, IModelBinder>(),
            NullLoggerFactory.Instance,
            allowValidatingTopLevelNodes: true);
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.IsType<PersonWithNoProperties>(bindingContext.Result.Model);
 
        var keyValuePair = Assert.Single(bindingContext.ModelState);
        Assert.Equal(string.Empty, keyValuePair.Key);
        var error = Assert.Single(keyValuePair.Value.Errors);
        Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
    }
 
    private IActionResult ActionWithAllPropertiesExcludedParameter(PersonWithAllPropertiesExcluded parameter) => null;
 
    [Fact]
    public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithAllPropertiesExcluded()
    {
        // Arrange
        var parameter = typeof(ComplexTypeModelBinderTest)
            .GetMethod(
                nameof(ActionWithAllPropertiesExcludedParameter),
                BindingFlags.Instance | BindingFlags.NonPublic)
            .GetParameters()[0];
        var metadataProvider = new TestModelMetadataProvider();
        metadataProvider
            .ForParameter(parameter)
            .BindingDetails(b => b.IsBindingRequired = true);
        var metadata = metadataProvider.GetMetadataForParameter(parameter);
        var bindingContext = new DefaultModelBindingContext
        {
            IsTopLevelObject = true,
            FieldName = "fieldName",
            ModelMetadata = metadata,
            ModelName = string.Empty,
            ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
            ModelState = new ModelStateDictionary(),
        };
 
        var binder = new ComplexTypeModelBinder(
            new Dictionary<ModelMetadata, IModelBinder>(),
            NullLoggerFactory.Instance,
            allowValidatingTopLevelNodes: true);
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.Result.IsModelSet);
        Assert.IsType<PersonWithAllPropertiesExcluded>(bindingContext.Result.Model);
 
        var keyValuePair = Assert.Single(bindingContext.ModelState);
        Assert.Equal(string.Empty, keyValuePair.Key);
        var error = Assert.Single(keyValuePair.Value.Errors);
        Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
    }
 
    [Theory]
    [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 = GetMetadataForProperty(typeof(MyModelTestingCanUpdateProperty), propertyName);
 
        // Act
        var canUpdate = ComplexTypeModelBinder.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 = _metadataProvider;
        var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName);
 
        // Act
        var canUpdate = ComplexTypeModelBinder.CanUpdatePropertyInternal(metadata);
 
        // Assert
        Assert.Equal(expected, canUpdate);
    }
 
    [Fact]
    public void CreateModel_InstantiatesInstanceOfMetadataType()
    {
        // Arrange
        var bindingContext = new DefaultModelBindingContext
        {
            ModelMetadata = GetMetadataForType(typeof(Person))
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var model = binder.CreateModelPublic(bindingContext);
 
        // Assert
        Assert.IsType<Person>(model);
    }
 
    [Fact]
    public void CreateModel_ForStructModelType_AsTopLevelObject_ThrowsException()
    {
        // Arrange
        var bindingContext = new DefaultModelBindingContext
        {
            ModelMetadata = GetMetadataForType(typeof(PointStruct)),
            IsTopLevelObject = true
        };
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() => binder.CreateModelPublic(bindingContext));
        Assert.Equal(
            string.Format(
                CultureInfo.CurrentCulture,
                "Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
                "value types and must have a parameterless constructor.",
                typeof(PointStruct).FullName),
            exception.Message);
    }
 
    [Fact]
    public void CreateModel_ForClassWithNoParameterlessConstructor_AsElement_ThrowsException()
    {
        // Arrange
        var expectedMessage = "Could not create an instance of type " +
            $"'{typeof(ClassWithNoParameterlessConstructor)}'. Model bound complex types must not be abstract " +
            "or value types and must have a parameterless constructor.";
        var metadata = GetMetadataForType(typeof(ClassWithNoParameterlessConstructor));
        var bindingContext = new DefaultModelBindingContext
        {
            ModelMetadata = metadata,
        };
        var binder = CreateBinder(metadata);
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() => binder.CreateModelPublic(bindingContext));
        Assert.Equal(expectedMessage, exception.Message);
    }
 
    [Fact]
    public void CreateModel_ForStructModelType_AsProperty_ThrowsException()
    {
        // Arrange
        var bindingContext = new DefaultModelBindingContext
        {
            ModelMetadata = GetMetadataForProperty(typeof(Location), nameof(Location.Point)),
            ModelName = nameof(Location.Point),
            IsTopLevelObject = false
        };
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() => binder.CreateModelPublic(bindingContext));
        Assert.Equal(
            string.Format(
                CultureInfo.CurrentCulture,
                "Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
                "value types and must have a parameterless constructor.",
                typeof(PointStruct).FullName),
            exception.Message);
    }
 
    [Fact]
    public async Task BindModelAsync_ModelIsNotNull_DoesNotCallCreateModel()
    {
        // Arrange
        var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person());
        var originalModel = bindingContext.Model;
        var binders = bindingContext.ModelMetadata.Properties.ToDictionary(
            keySelector: item => item,
            elementSelector: item => (IModelBinder)null);
 
        var binder = new Mock<TestableComplexTypeModelBinder>(binders) { CallBase = true };
        binder
            .Setup(b => b.CreateModelPublic(It.IsAny<ModelBindingContext>()))
            .Verifiable();
 
        // Act
        await binder.Object.BindModelAsync(bindingContext);
 
        // Assert
        Assert.Same(originalModel, bindingContext.Model);
        binder.Verify(o => o.CreateModelPublic(bindingContext), Times.Never());
    }
 
    [Fact]
    public async Task BindModelAsync_ModelIsNull_CallsCreateModel()
    {
        // Arrange
        var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null);
        var binders = bindingContext.ModelMetadata.Properties.ToDictionary(
            keySelector: item => item,
            elementSelector: item => (IModelBinder)null);
 
        var testableBinder = new Mock<TestableComplexTypeModelBinder>(binders) { CallBase = true };
        testableBinder
            .Setup(o => o.CreateModelPublic(bindingContext))
            .Returns(new Person())
            .Verifiable();
 
        // Act
        await testableBinder.Object.BindModelAsync(bindingContext);
 
        // Assert
        Assert.NotNull(bindingContext.Model);
        Assert.IsType<Person>(bindingContext.Model);
        testableBinder.Verify();
    }
 
    [Theory]
    [InlineData(nameof(PersonWithBindExclusion.FirstName))]
    [InlineData(nameof(PersonWithBindExclusion.LastName))]
    public void CanBindProperty_GetSetProperty(string property)
    {
        // Arrange
        var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
        var bindingContext = new DefaultModelBindingContext()
        {
            ActionContext = new ActionContext()
            {
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = new ServiceCollection().BuildServiceProvider(),
                },
            },
            ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var result = binder.CanBindPropertyPublic(bindingContext, metadata);
 
        // Assert
        Assert.True(result);
    }
 
    [Theory]
    [InlineData(nameof(PersonWithBindExclusion.NonUpdateableProperty))]
    public void CanBindProperty_GetOnlyProperty_WithBindNever(string property)
    {
        // Arrange
        var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
        var bindingContext = new DefaultModelBindingContext()
        {
            ActionContext = new ActionContext()
            {
                HttpContext = new DefaultHttpContext()
                {
                    RequestServices = new ServiceCollection().BuildServiceProvider(),
                },
            },
            ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var result = binder.CanBindPropertyPublic(bindingContext, metadata);
 
        // Assert
        Assert.False(result);
    }
 
    [Theory]
    [InlineData(nameof(PersonWithBindExclusion.DateOfBirth))]
    [InlineData(nameof(PersonWithBindExclusion.DateOfDeath))]
    public void CanBindProperty_GetSetProperty_WithBindNever(string property)
    {
        // Arrange
        var metadata = GetMetadataForProperty(typeof(PersonWithBindExclusion), property);
        var bindingContext = new DefaultModelBindingContext()
        {
            ActionContext = new ActionContext()
            {
                HttpContext = new DefaultHttpContext(),
            },
            ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion)),
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var result = binder.CanBindPropertyPublic(bindingContext, metadata);
 
        // Assert
        Assert.False(result);
    }
 
    [Theory]
    [InlineData(nameof(TypeWithIncludedPropertiesUsingBindAttribute.IncludedExplicitly1), true)]
    [InlineData(nameof(TypeWithIncludedPropertiesUsingBindAttribute.IncludedExplicitly2), true)]
    [InlineData(nameof(TypeWithIncludedPropertiesUsingBindAttribute.ExcludedByDefault1), false)]
    [InlineData(nameof(TypeWithIncludedPropertiesUsingBindAttribute.ExcludedByDefault2), false)]
    public void CanBindProperty_WithBindInclude(string property, bool expected)
    {
        // Arrange
        var metadata = GetMetadataForProperty(typeof(TypeWithIncludedPropertiesUsingBindAttribute), property);
        var bindingContext = new DefaultModelBindingContext()
        {
            ActionContext = new ActionContext()
            {
                HttpContext = new DefaultHttpContext()
            },
            ModelMetadata = GetMetadataForType(typeof(TypeWithIncludedPropertiesUsingBindAttribute)),
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var result = binder.CanBindPropertyPublic(bindingContext, metadata);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Theory]
    [InlineData(nameof(ModelWithMixedBindingBehaviors.Required), true)]
    [InlineData(nameof(ModelWithMixedBindingBehaviors.Optional), true)]
    [InlineData(nameof(ModelWithMixedBindingBehaviors.Never), false)]
    public void CanBindProperty_BindingAttributes_OverridingBehavior(string property, bool expected)
    {
        // Arrange
        var metadata = GetMetadataForProperty(typeof(ModelWithMixedBindingBehaviors), property);
        var bindingContext = new DefaultModelBindingContext()
        {
            ActionContext = new ActionContext()
            {
                HttpContext = new DefaultHttpContext(),
            },
            ModelMetadata = GetMetadataForType(typeof(ModelWithMixedBindingBehaviors)),
        };
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        var result = binder.CanBindPropertyPublic(bindingContext, metadata);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Fact]
    [ReplaceCulture]
    public async Task BindModelAsync_BindRequiredFieldMissing_RaisesModelError()
    {
        // Arrange
        var model = new ModelWithBindRequired
        {
            Name = "original value",
            Age = -20
        };
 
        var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
 
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
        binder.Results[property] = ModelBindingResult.Failed();
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        var modelStateDictionary = bindingContext.ModelState;
        Assert.False(modelStateDictionary.IsValid);
        Assert.Single(modelStateDictionary);
 
        // Check Age error.
        Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out var entry));
        var modelError = Assert.Single(entry.Errors);
        Assert.Null(modelError.Exception);
        Assert.NotNull(modelError.ErrorMessage);
        Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
    }
 
    [Fact]
    [ReplaceCulture]
    public async Task BindModelAsync_DataMemberIsRequiredFieldMissing_RaisesModelError()
    {
        // Arrange
        var model = new ModelWithDataMemberIsRequired
        {
            Name = "original value",
            Age = -20
        };
 
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithDataMemberIsRequired.Age));
        binder.Results[property] = ModelBindingResult.Failed();
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        var modelStateDictionary = bindingContext.ModelState;
        Assert.False(modelStateDictionary.IsValid);
        Assert.Single(modelStateDictionary);
 
        // Check Age error.
        Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out var entry));
        var modelError = Assert.Single(entry.Errors);
        Assert.Null(modelError.Exception);
        Assert.NotNull(modelError.ErrorMessage);
        Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
    }
 
    [Fact]
    [ReplaceCulture]
    public async Task BindModelAsync_ValueTypePropertyWithBindRequired_SetToNull_CapturesException()
    {
        // Arrange
        var model = new ModelWithBindRequired
        {
            Name = "original value",
            Age = -20
        };
 
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this
        // case because the property did have a result.
        var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithBindRequired.Age));
        binder.Results[property] = ModelBindingResult.Success(model: null);
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        var modelStateDictionary = bindingContext.ModelState;
        Assert.False(modelStateDictionary.IsValid);
        Assert.Single(modelStateDictionary);
 
        // Check Age error.
        Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out var entry));
        Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
 
        var modelError = Assert.Single(entry.Errors);
        Assert.Equal(string.Empty, modelError.ErrorMessage);
        Assert.IsType<NullReferenceException>(modelError.Exception);
    }
 
    [Fact]
    public async Task BindModelAsync_ValueTypeProperty_WithBindingOptional_NoValueSet_NoError()
    {
        // Arrange
        var model = new BindingOptionalProperty();
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        var property = GetMetadataForProperty(model.GetType(), nameof(BindingOptionalProperty.ValueTypeRequired));
        binder.Results[property] = ModelBindingResult.Failed();
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        var modelStateDictionary = bindingContext.ModelState;
        Assert.True(modelStateDictionary.IsValid);
    }
 
    [Fact]
    public async Task BindModelAsync_NullableValueTypeProperty_NoValueSet_NoError()
    {
        // Arrange
        var model = new NullableValueTypeProperty();
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        var property = GetMetadataForProperty(model.GetType(), nameof(NullableValueTypeProperty.NullableValueType));
        binder.Results[property] = ModelBindingResult.Failed();
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        var modelStateDictionary = bindingContext.ModelState;
        Assert.True(modelStateDictionary.IsValid);
    }
 
    [Fact]
    public async Task BindModelAsync_ValueTypeProperty_NoValue_NoError()
    {
        // Arrange
        var model = new Person();
        var containerMetadata = GetMetadataForType(model.GetType());
 
        var bindingContext = CreateContext(containerMetadata, model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
        binder.Results[property] = ModelBindingResult.Failed();
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.ModelState.IsValid);
        Assert.Equal(0, model.ValueTypeRequired);
    }
 
    [Fact]
    public async Task BindModelAsync_ProvideRequiredField_Success()
    {
        // Arrange
        var model = new Person();
        var containerMetadata = GetMetadataForType(model.GetType());
 
        var bindingContext = CreateContext(containerMetadata, model);
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        var property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
        binder.Results[property] = ModelBindingResult.Success(model: 57);
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.True(bindingContext.ModelState.IsValid);
        Assert.Equal(57, model.ValueTypeRequired);
    }
 
    [Fact]
    public async Task BindModelAsync_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 binder = CreateBinder(bindingContext.ModelMetadata);
 
        foreach (var property in containerMetadata.Properties)
        {
            binder.Results[property] = ModelBindingResult.Failed();
        }
 
        var firstNameProperty = containerMetadata.Properties[nameof(model.FirstName)];
        binder.Results[firstNameProperty] = ModelBindingResult.Success("John");
 
        var lastNameProperty = containerMetadata.Properties[nameof(model.LastName)];
        binder.Results[lastNameProperty] = ModelBindingResult.Success("Doe");
 
        // Act
        await binder.BindModelAsync(bindingContext);
 
        // Assert
        Assert.Equal("John", model.FirstName);
        Assert.Equal("Doe", model.LastName);
        Assert.Equal(dob, model.DateOfBirth);
        Assert.True(bindingContext.ModelState.IsValid);
    }
 
    // Validates fix for https://github.com/dotnet/aspnetcore/issues/21916
    [Fact]
    public async Task BindModelAsync_PropertyInitializedInNonParameterlessConstructorConstructor()
    {
        // Arrange
        var model = new ModelWithPropertyInitializedInConstructor("TestName");
        var property = GetMetadataForProperty(model.GetType(), nameof(ModelWithPropertyInitializedInConstructor.NameContainer));
        var nestedProperty = GetMetadataForProperty(typeof(ClassWithNoParameterlessConstructor), nameof(ClassWithNoParameterlessConstructor.Name));
        var bindingContext = CreateContext(property);
        bindingContext.IsTopLevelObject = false;
        var valueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
        valueProvider
            .Setup(provider => provider.ContainsPrefix("theModel.Name"))
            .Returns(true);
        bindingContext.ValueProvider = valueProvider.Object;
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        binder.Results[nestedProperty] = ModelBindingResult.Success(null);
 
        // Act
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await binder.BindModelAsync(bindingContext));
        // Assert
        var unexpectedMessage = "Alternatively, set the 'NameContainer' property to a non-null value in the 'Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderTest+ModelWithPropertyInitializedInConstructor' constructor.";
        Assert.DoesNotContain(exception.Message, unexpectedMessage);
    }
 
    [Fact]
    public void SetProperty_PropertyHasDefaultValue_DefaultValueAttributeDoesNothing()
    {
        // Arrange
        var model = new Person();
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
 
        var metadata = GetMetadataForType(typeof(Person));
        var propertyMetadata = metadata.Properties[nameof(model.PropertyWithDefaultValue)];
 
        var result = ModelBindingResult.Failed();
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // Assert
        var person = Assert.IsType<Person>(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 metadata = GetMetadataForType(typeof(Person));
        var propertyMetadata = metadata.Properties[nameof(model.PropertyWithInitializedValue)];
 
        // The null model value won't be used because IsModelBound = false.
        var result = ModelBindingResult.Failed();
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // Assert
        var person = Assert.IsType<Person>(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 metadata = GetMetadataForType(typeof(Person));
        var propertyMetadata = metadata.Properties[nameof(model.PropertyWithInitializedValueAndDefault)];
 
        // The null model value won't be used because IsModelBound = false.
        var result = ModelBindingResult.Failed();
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // Assert
        var person = Assert.IsType<Person>(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 metadata = GetMetadataForType(typeof(Person));
        var propertyMetadata = metadata.Properties[nameof(model.NonUpdateableProperty)];
 
        var result = ModelBindingResult.Failed();
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // Assert
        // If didn't throw, success!
    }
 
    // Property name, property accessor
    public static TheoryData<string, Func<object, object>> MyCanUpdateButCannotSetPropertyData
    {
        get
        {
            return new TheoryData<string, Func<object, object>>
                {
                    {
                        nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject),
                        model => ((Simple)((MyModelTestingCanUpdateProperty)model).ReadOnlyObject).Name
                    },
                    {
                        nameof(MyModelTestingCanUpdateProperty.ReadOnlySimple),
                        model => ((MyModelTestingCanUpdateProperty)model).ReadOnlySimple.Name
                    },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(MyCanUpdateButCannotSetPropertyData))]
    public void SetProperty_ValueProvidedAndCanUpdatePropertyTrue_DoesNothing(
        string propertyName,
        Func<object, object> propertyAccessor)
    {
        // Arrange
        var model = new MyModelTestingCanUpdateProperty();
        var type = model.GetType();
        var bindingContext = CreateContext(GetMetadataForType(type), model);
        var modelState = bindingContext.ModelState;
        var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName];
        var result = ModelBindingResult.Success(new Simple { Name = "Hanna" });
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, propertyName, propertyMetadata, result);
 
        // Assert
        Assert.Equal("Joe", propertyAccessor(model));
        Assert.True(modelState.IsValid);
        Assert.Empty(modelState);
    }
 
    [Fact]
    public void SetProperty_ReadOnlyProperty_IsNoOp()
    {
        // Arrange
        var model = new CollectionContainer();
        var originalCollection = model.ReadOnlyList;
 
        var modelMetadata = GetMetadataForType(model.GetType());
        var propertyMetadata = GetMetadataForProperty(model.GetType(), nameof(CollectionContainer.ReadOnlyList));
 
        var bindingContext = CreateContext(modelMetadata, model);
        var result = ModelBindingResult.Success(new List<string>() { "hi" });
 
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, propertyMetadata.PropertyName, propertyMetadata, result);
 
        // Assert
        Assert.Same(originalCollection, model.ReadOnlyList);
        Assert.Empty(model.ReadOnlyList);
    }
 
    [Fact]
    public void SetProperty_PropertyIsSettable_CallsSetter()
    {
        // Arrange
        var model = new Person();
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
        var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)];
 
        var result = ModelBindingResult.Success(new DateTime(2001, 1, 1));
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // 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 propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)];
 
        var result = ModelBindingResult.Success(new DateTime(1800, 1, 1));
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo", propertyMetadata, result);
 
        // Assert
        Assert.Equal("Date of death can't be before date of birth. (Parameter 'value')",
                     bindingContext.ModelState["foo"].Errors[0].Exception.Message);
    }
 
    [Fact]
    [ReplaceCulture]
    public void SetProperty_PropertySetterThrows_CapturesException()
    {
        // Arrange
        var model = new ModelWhosePropertySetterThrows();
        var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
        bindingContext.ModelName = "foo";
        var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.NameNoAttribute)];
 
        var result = ModelBindingResult.Success(model: null);
        var binder = CreateBinder(bindingContext.ModelMetadata);
 
        // Act
        binder.SetPropertyPublic(bindingContext, "foo.NameNoAttribute", propertyMetadata, result);
 
        // Assert
        Assert.False(bindingContext.ModelState.IsValid);
        Assert.Single(bindingContext.ModelState["foo.NameNoAttribute"].Errors);
        Assert.Equal("This is a different exception. (Parameter 'value')",
                     bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
    }
 
    private static TestableComplexTypeModelBinder CreateBinder(ModelMetadata metadata)
    {
        var options = Options.Create(new MvcOptions());
        var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory());
        setup.Configure(options.Value);
 
        var lastIndex = options.Value.ModelBinderProviders.Count - 1;
        options.Value.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
        options.Value.ModelBinderProviders.Add(new TestableComplexTypeModelBinderProvider());
 
        var factory = TestModelBinderFactory.Create(options.Value.ModelBinderProviders.ToArray());
        return (TestableComplexTypeModelBinder)factory.CreateBinder(new ModelBinderFactoryContext()
        {
            Metadata = metadata,
            BindingInfo = new BindingInfo()
            {
                BinderModelName = metadata.BinderModelName,
                BinderType = metadata.BinderType,
                BindingSource = metadata.BindingSource,
                PropertyFilterProvider = metadata.PropertyFilterProvider,
            },
        });
    }
 
    private static DefaultModelBindingContext CreateContext(ModelMetadata metadata, object model = null)
    {
        var valueProvider = new TestValueProvider(new Dictionary<string, object>());
        return new DefaultModelBindingContext()
        {
            BinderModelName = metadata.BinderModelName,
            BindingSource = metadata.BindingSource,
            IsTopLevelObject = true,
            Model = model,
            ModelMetadata = metadata,
            ModelName = "theModel",
            ModelState = new ModelStateDictionary(),
            ValueProvider = valueProvider,
        };
    }
 
    private static ModelMetadata GetMetadataForType(Type type)
    {
        return _metadataProvider.GetMetadataForType(type);
    }
 
    private static ModelMetadata GetMetadataForProperty(Type type, string propertyName)
    {
        return _metadataProvider.GetMetadataForProperty(type, propertyName);
    }
 
    private class Location
    {
        public PointStruct Point { get; set; }
    }
 
    private readonly struct PointStruct
    {
        public PointStruct(double x, double y)
        {
            X = x;
            Y = y;
        }
 
        public double X { get; }
        public double Y { get; }
    }
 
    private class ClassWithNoParameterlessConstructor
    {
        public ClassWithNoParameterlessConstructor(string name)
        {
            Name = name;
        }
 
        public string Name { get; set; }
    }
 
    private class ModelWithPropertyInitializedInConstructor
    {
        public ModelWithPropertyInitializedInConstructor(string name)
        {
            NameContainer = new ClassWithNoParameterlessConstructor(name);
        }
 
        [ValueBinderMetadataAttribute]
        public ClassWithNoParameterlessConstructor NameContainer { get; set; }
    }
 
    private class BindingOptionalProperty
    {
        [BindingBehavior(BindingBehavior.Optional)]
        public int ValueTypeRequired { get; set; }
    }
 
    private class NullableValueTypeProperty
    {
        [BindingBehavior(BindingBehavior.Optional)]
        public int? NullableValueType { get; set; }
    }
 
    private class Person
    {
        private DateTime? _dateOfDeath;
 
        [BindingBehavior(BindingBehavior.Optional)]
        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; }
 
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string NonUpdateableProperty { get; private set; }
 
        [BindingBehavior(BindingBehavior.Optional)]
        [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 = null;
    }
 
    private class PersonWithAllPropertiesExcluded
    {
        [BindNever]
        public DateTime DateOfBirth { get; set; }
 
        [BindNever]
        public DateTime? DateOfDeath { get; set; }
 
        [BindNever]
        public string FirstName { get; set; }
 
        [BindNever]
        public string LastName { get; set; }
 
        public string NonUpdateableProperty { get; private 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 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; }
    }
 
    private sealed class MyModelTestingCanUpdateProperty
    {
        public int ReadOnlyInt { get; private set; }
        public string ReadOnlyString { 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 HasAllGreedyProperties
    {
        [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; }
    }
 
    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 new BindingSource("Special", string.Empty, isGreedy: true, isFromRequest: true); }
        }
    }
 
    private class ValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata
    {
        public BindingSource BindingSource { get { return BindingSource.Query; } }
    }
 
    private class ExcludedProvider : IPropertyFilterProvider
    {
        public Func<ModelMetadata, bool> PropertyFilter
        {
            get
            {
                return (m) =>
                   !string.Equals("Excluded1", m.PropertyName, StringComparison.OrdinalIgnoreCase) &&
                   !string.Equals("Excluded2", m.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<int, string> ReadOnlyDictionary { get; } = new Dictionary<int, string>();
 
        public IList<int> ReadOnlyList { get; } = new List<int>();
 
        // Settable values are overwritten.
        public int[] SettableArray { get; set; } = new int[] { 0, 1 };
 
        public IDictionary<int, string> SettableDictionary { get; set; } = new Dictionary<int, string>
            {
                { 0, "zero" },
                { 25, "twenty-five" },
            };
 
        public IList<int> SettableList { get; set; } = new List<int> { 3, 9, 0 };
    }
 
    private class TestableComplexTypeModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.IsComplexType)
            {
                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
                foreach (var property in context.Metadata.Properties)
                {
                    propertyBinders.Add(property, context.CreateBinder(property));
                }
 
                return new TestableComplexTypeModelBinder(propertyBinders);
            }
 
            return null;
        }
    }
 
    // Provides the ability to easily mock + call each of these APIs
    public class TestableComplexTypeModelBinder : ComplexTypeModelBinder
    {
        public TestableComplexTypeModelBinder()
            : this(new Dictionary<ModelMetadata, IModelBinder>())
        {
        }
 
        public TestableComplexTypeModelBinder(bool allowValidatingTopLevelNodes)
            : this(new Dictionary<ModelMetadata, IModelBinder>(), allowValidatingTopLevelNodes)
        {
        }
 
        public TestableComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
            : base(propertyBinders, NullLoggerFactory.Instance)
        {
        }
 
        public TestableComplexTypeModelBinder(
            IDictionary<ModelMetadata, IModelBinder> propertyBinders,
            bool allowValidatingTopLevelNodes)
            : base(propertyBinders, NullLoggerFactory.Instance, allowValidatingTopLevelNodes)
        {
        }
 
        public Dictionary<ModelMetadata, ModelBindingResult> Results { get; } = new Dictionary<ModelMetadata, ModelBindingResult>();
 
        public virtual Task BindPropertyPublic(ModelBindingContext bindingContext)
        {
            if (Results.Count == 0)
            {
                return base.BindModelAsync(bindingContext);
            }
 
            if (Results.TryGetValue(bindingContext.ModelMetadata, out var result))
            {
                bindingContext.Result = result;
            }
 
            return Task.CompletedTask;
        }
 
        protected override Task BindProperty(ModelBindingContext bindingContext)
        {
            return BindPropertyPublic(bindingContext);
        }
 
        public virtual bool CanBindPropertyPublic(
            ModelBindingContext bindingContext,
            ModelMetadata propertyMetadata)
        {
            if (Results.Count == 0)
            {
                return base.CanBindProperty(bindingContext, propertyMetadata);
            }
 
            // If this is being used to test binding, then only attempt to bind properties
            // we have results for.
            return Results.ContainsKey(propertyMetadata);
        }
 
        protected override bool CanBindProperty(
            ModelBindingContext bindingContext,
            ModelMetadata propertyMetadata)
        {
            return CanBindPropertyPublic(bindingContext, propertyMetadata);
        }
 
        public virtual object CreateModelPublic(ModelBindingContext bindingContext)
        {
            return base.CreateModel(bindingContext);
        }
 
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            return CreateModelPublic(bindingContext);
        }
 
        public virtual void SetPropertyPublic(
            ModelBindingContext bindingContext,
            string modelName,
            ModelMetadata propertyMetadata,
            ModelBindingResult result)
        {
            base.SetProperty(bindingContext, modelName, propertyMetadata, result);
        }
 
        protected override void SetProperty(
            ModelBindingContext bindingContext,
            string modelName,
            ModelMetadata propertyMetadata,
            ModelBindingResult result)
        {
            SetPropertyPublic(bindingContext, modelName, propertyMetadata, result);
        }
    }
}