Bladeren bron

Merge pull request #24258 from dotnet-maestro-bot/merge/release/5.0-preview8-to-master

[automated] Merge branch 'release/5.0-preview8' => 'master'
Tanay Parikh 5 jaren geleden
bovenliggende
commit
3ea1fc7aac
100 gewijzigde bestanden met toevoegingen van 14925 en 4365 verwijderingen
  1. 0 30
      src/Identity/Core/src/IdentityServiceCollectionExtensions.cs
  2. 6 1
      src/Mvc/Mvc.Abstractions/src/ModelBinding/IPropertyFilterProvider.cs
  3. 2 2
      src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelBindingMessageProvider.cs
  4. 38 2
      src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataIdentity.cs
  5. 6 1
      src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataKind.cs
  6. 96 1
      src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs
  7. 12 1
      src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadataProvider.cs
  8. 1 0
      src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs
  9. 7 7
      src/Mvc/Mvc.Abstractions/test/ModelBinding/BindingInfoTest.cs
  10. 6 0
      src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelMetadataTest.cs
  11. 1 1
      src/Mvc/Mvc.Analyzers/test/TestFiles/TopLevelParameterNameAnalyzerTest/IsProblematicParameter_ReturnsTrue_IfPropertyWithModelBindingAttributeHasSameNameAsParameter.cs
  12. 12 5
      src/Mvc/Mvc.Core/src/BindAttribute.cs
  13. 1 1
      src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs
  14. 1 1
      src/Mvc/Mvc.Core/src/Infrastructure/ParameterDefaultValues.cs
  15. 752 0
      src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs
  16. 64 0
      src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinderProvider.cs
  17. 1 0
      src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs
  18. 2 1
      src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinderProvider.cs
  19. 6 0
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/BindingMetadata.cs
  20. 74 0
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultBindingMetadataProvider.cs
  21. 11 1
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultMetadataDetails.cs
  22. 37 1
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultModelMetadata.cs
  23. 99 7
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
  24. 5 0
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultValidationMetadataProvider.cs
  25. 10 0
      src/Mvc/Mvc.Core/src/ModelBinding/Metadata/ModelAttributes.cs
  26. 1 1
      src/Mvc/Mvc.Core/src/ModelBinding/ParameterBinder.cs
  27. 54 35
      src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs
  28. 6 5
      src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidateNeverAttribute.cs
  29. 12 0
      src/Mvc/Mvc.Core/src/Resources.resx
  30. 2 2
      src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs
  31. 1 1
      src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs
  32. 2 1
      src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
  33. 92 0
      src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexObjectModelBinderProviderTest.cs
  34. 1412 0
      src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexObjectModelBinderTest.cs
  35. 3 1
      src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderProviderTest.cs
  36. 3 2
      src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs
  37. 3 2
      src/Mvc/Mvc.Core/test/ModelBinding/Binders/DictionaryModelBinderTest.cs
  38. 193 1
      src/Mvc/Mvc.Core/test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs
  39. 313 44
      src/Mvc/Mvc.Core/test/ModelBinding/Metadata/DefaultModelMetadataTest.cs
  40. 7 7
      src/Mvc/Mvc.Core/test/ModelBinding/ModelBindingHelperTest.cs
  41. 4 4
      src/Mvc/Mvc.DataAnnotations/test/ModelMetadataProviderTest.cs
  42. 1 1
      src/Mvc/Mvc/test/MvcOptionsSetupTest.cs
  43. 34 0
      src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs
  44. 66 0
      src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs
  45. 11 8
      src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonInputFormatterTest.cs
  46. 208 8
      src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs
  47. 13 0
      src/Mvc/test/Mvc.IntegrationTests/ComplexObjectIntegrationTest.cs
  48. 4269 0
      src/Mvc/test/Mvc.IntegrationTests/ComplexRecordIntegrationTest.cs
  49. 3774 0
      src/Mvc/test/Mvc.IntegrationTests/ComplexTypeIntegrationTestBase.cs
  50. 16 3727
      src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs
  51. 1 0
      src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj
  52. 174 1
      src/Mvc/test/Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs
  53. 2307 0
      src/Mvc/test/Mvc.IntegrationTests/ValidationWithRecordIntegrationTests.cs
  54. 28 1
      src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs
  55. 2 1
      src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj
  56. 4 2
      src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs
  57. 5 0
      src/Mvc/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Controllers/HtmlGeneration_CustomerController.cs
  58. 1 0
      src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj
  59. 29 0
      src/Mvc/test/WebSites/HtmlGenerationWebSite/Models/CustomerRecord.cs
  60. 46 0
      src/Mvc/test/WebSites/HtmlGenerationWebSite/Views/Shared/CustomerWithRecords.cshtml
  61. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Properties/launchSettings.json
  62. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Properties/launchSettings.json
  63. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Properties/launchSettings.json
  64. 7 6
      src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json
  65. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json
  66. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/Properties/launchSettings.json
  67. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json
  68. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json
  69. 8 7
      src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json
  70. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json
  71. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json
  72. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/Properties/launchSettings.json
  73. 1 0
      src/ProjectTemplates/Web.ProjectTemplates/content/Worker-FSharp/Properties/launchSettings.json
  74. 2 38
      src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs
  75. 4 3
      src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs
  76. 3 24
      src/Security/Authentication/Cookies/src/CookieExtensions.cs
  77. 9 86
      src/Security/Authentication/Core/src/AuthenticationBuilder.cs
  78. 1 22
      src/Security/Authentication/Facebook/src/FacebookExtensions.cs
  79. 1 22
      src/Security/Authentication/Google/src/GoogleExtensions.cs
  80. 1 22
      src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
  81. 2 23
      src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs
  82. 1 46
      src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs
  83. 1 31
      src/Security/Authentication/OAuth/src/OAuthExtensions.cs
  84. 1 22
      src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs
  85. 1 22
      src/Security/Authentication/Twitter/src/TwitterExtensions.cs
  86. 1 46
      src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs
  87. 108 0
      src/Tools/dotnet-watch/src/BrowserRefreshServer.cs
  88. 18 1
      src/Tools/dotnet-watch/src/DotNetWatcher.cs
  89. 3 1
      src/Tools/dotnet-watch/src/IFileSet.cs
  90. 5 2
      src/Tools/dotnet-watch/src/Internal/FileSet.cs
  91. 7 2
      src/Tools/dotnet-watch/src/Internal/MsBuildFileSetFactory.cs
  92. 29 17
      src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs
  93. 219 0
      src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs
  94. 21 0
      src/Tools/dotnet-watch/src/LaunchSettingsJson.cs
  95. 7 1
      src/Tools/dotnet-watch/src/ProcessSpec.cs
  96. 2 2
      src/Tools/dotnet-watch/src/Program.cs
  97. 14 1
      src/Tools/dotnet-watch/src/assets/DotNetWatch.targets
  98. 20 0
      src/Tools/dotnet-watch/src/dotnet-watch.csproj
  99. 3 0
      src/Tools/dotnet-watch/src/runtimeconfig.template.json
  100. 68 0
      src/Tools/dotnet-watch/test/BrowserLaunchTests.cs

+ 0 - 30
src/Identity/Core/src/IdentityServiceCollectionExtensions.cs

@@ -110,21 +110,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
             => services.Configure(IdentityConstants.ApplicationScheme, configure);
 
-        /// <summary>
-        /// Configures the application cookie.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="services">The services available in the application.</param>
-        /// <param name="configure">An action to configure the <see cref="CookieAuthenticationOptions"/>.</param>
-        /// <returns>The services.</returns>
-        public static IServiceCollection ConfigureApplicationCookie<TService>(this IServiceCollection services, Action<CookieAuthenticationOptions, TService> configure) where TService : class
-        {
-            services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
-                .Configure(configure);
-
-            return services;
-        }
-
         /// <summary>
         /// Configure the external cookie.
         /// </summary>
@@ -133,20 +118,5 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <returns>The services.</returns>
         public static IServiceCollection ConfigureExternalCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
             => services.Configure(IdentityConstants.ExternalScheme, configure);
-
-        /// <summary>
-        /// Configure the external cookie.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="services">The services available in the application.</param>
-        /// <param name="configure">An action to configure the <see cref="CookieAuthenticationOptions"/>.</param>
-        /// <returns>The services.</returns>
-        public static IServiceCollection ConfigureExternalCookie<TService>(this IServiceCollection services, Action<CookieAuthenticationOptions, TService> configure) where TService : class
-        {
-            services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ExternalScheme)
-                .Configure(configure);
-
-            return services;
-        }
     }
 }

+ 6 - 1
src/Mvc/Mvc.Abstractions/src/ModelBinding/IPropertyFilterProvider.cs

@@ -6,12 +6,17 @@ using System;
 namespace Microsoft.AspNetCore.Mvc.ModelBinding
 {
     /// <summary>
-    /// Provides a predicate which can determines which model properties should be bound by model binding.
+    /// Provides a predicate which can determines which model properties or parameters should be bound by model binding.
     /// </summary>
     public interface IPropertyFilterProvider
     {
         /// <summary>
+        /// <para>
         /// Gets a predicate which can determines which model properties should be bound by model binding.
+        /// </para>
+        /// <para>
+        /// This predicate is also used to determine which parameters are bound when a model's constructor is bound.
+        /// </para>
         /// </summary>
         Func<ModelMetadata, bool> PropertyFilter { get; }
     }

+ 2 - 2
src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelBindingMessageProvider.cs

@@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// <summary>
         /// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
         /// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
-        /// with a collection element or action parameter.
+        /// with a collection element or parameter.
         /// </summary>
         /// <value>Default <see cref="string"/> is "The value '{0}' is not valid.".</value>
         public virtual Func<string, string> NonPropertyAttemptedValueIsInvalidAccessor { get; } = default!;
@@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// <summary>
         /// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
         /// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
-        /// with a collection element or action parameter.
+        /// with a collection element or parameter.
         /// </summary>
         /// <value>Default <see cref="string"/> is "The supplied value is invalid.".</value>
         public virtual Func<string> NonPropertyUnknownValueIsInvalidAccessor { get; } = default!;

+ 38 - 2
src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataIdentity.cs

@@ -16,12 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             Type modelType,
             string? name = null,
             Type? containerType = null,
-            object? fieldInfo = null)
+            object? fieldInfo = null,
+            ConstructorInfo? constructorInfo = null)
         {
             ModelType = modelType;
             Name = name;
             ContainerType = containerType;
             FieldInfo = fieldInfo;
+            ConstructorInfo = constructorInfo;
         }
 
         /// <summary>
@@ -130,6 +132,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             return new ModelMetadataIdentity(modelType, parameter.Name, fieldInfo: parameter);
         }
 
+        /// <summary>
+        /// Creates a <see cref="ModelMetadataIdentity"/> for the provided parameter with the specified
+        /// model type.
+        /// </summary>
+        /// <param name="constructor">The <see cref="ConstructorInfo" />.</param>
+        /// <param name="modelType">The model type.</param>
+        /// <returns>A <see cref="ModelMetadataIdentity"/>.</returns>
+        public static ModelMetadataIdentity ForConstructor(ConstructorInfo constructor, Type modelType)
+        {
+            if (constructor == null)
+            {
+                throw new ArgumentNullException(nameof(constructor));
+            }
+
+            if (modelType == null)
+            {
+                throw new ArgumentNullException(nameof(modelType));
+            }
+
+            return new ModelMetadataIdentity(modelType, constructor.Name, constructorInfo: constructor);
+        }
+
         /// <summary>
         /// Gets the <see cref="Type"/> defining the model property represented by the current
         /// instance, or <c>null</c> if the current instance does not represent a property.
@@ -152,6 +176,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
                 {
                     return ModelMetadataKind.Parameter;
                 }
+                else if (ConstructorInfo != null)
+                {
+                    return ModelMetadataKind.Constructor;
+                }
                 else if (ContainerType != null && Name != null)
                 {
                     return ModelMetadataKind.Property;
@@ -183,6 +211,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// </summary>
         public PropertyInfo? PropertyInfo => FieldInfo as PropertyInfo;
 
+        /// <summary>
+        /// Gets a descriptor for the constructor, or <c>null</c> if this instance
+        /// does not represent a constructor.
+        /// </summary>
+        public ConstructorInfo? ConstructorInfo { get; }
+
         /// <inheritdoc />
         public bool Equals(ModelMetadataIdentity other)
         {
@@ -191,7 +225,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
                 ModelType == other.ModelType &&
                 Name == other.Name &&
                 ParameterInfo == other.ParameterInfo && 
-                PropertyInfo == other.PropertyInfo;
+                PropertyInfo == other.PropertyInfo &&
+                ConstructorInfo == other.ConstructorInfo;
         }
 
         /// <inheritdoc />
@@ -210,6 +245,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             hash.Add(Name, StringComparer.Ordinal);
             hash.Add(ParameterInfo);
             hash.Add(PropertyInfo);
+            hash.Add(ConstructorInfo);
             return hash.ToHashCode();
         }
     }

+ 6 - 1
src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataKind.cs

@@ -22,5 +22,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// Used for <see cref="ModelMetadata"/> for a parameter.
         /// </summary>
         Parameter,
+
+        /// <summary>
+        /// <see cref="ModelMetadata"/> for a constructor.
+        /// </summary>
+        Constructor,
     }
-}
+}

+ 96 - 1
src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs

@@ -4,8 +4,10 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Diagnostics;
+using System.Linq;
 using System.Reflection;
 using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
 using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -24,7 +26,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         /// </summary>
         public static readonly int DefaultOrder = 10000;
 
+        private static readonly IReadOnlyDictionary<ModelMetadata, ModelMetadata> EmptyParameterMapping = new Dictionary<ModelMetadata, ModelMetadata>(0);
+
         private int? _hashCode;
+        private IReadOnlyList<ModelMetadata>? _boundProperties;
+        private IReadOnlyDictionary<ModelMetadata, ModelMetadata>? _parameterMapping;
 
         /// <summary>
         /// Creates a new <see cref="ModelMetadata"/>.
@@ -83,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         /// <summary>
         /// Gets the key for the current instance.
         /// </summary>
-        protected ModelMetadataIdentity Identity { get; }
+        protected internal ModelMetadataIdentity Identity { get; }
 
         /// <summary>
         /// Gets a collection of additional information about the model.
@@ -95,6 +101,88 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         /// </summary>
         public abstract ModelPropertyCollection Properties { get; }
 
+        internal IReadOnlyList<ModelMetadata> BoundProperties
+        {
+            get
+            {
+                // In record types, each constructor parameter in the primary constructor is also a settable property with the same name.
+                // Executing model binding on these parameters twice may have detrimental effects, such as duplicate ModelState entries,
+                // or failures if a model expects to be bound exactly ones.
+                // Consequently when binding to a constructor, we only bind and validate the subset of properties whose names
+                // haven't appeared as parameters.
+                if (BoundConstructor is null)
+                {
+                    return Properties;
+                }
+
+                if (_boundProperties is null)
+                {
+                    var boundParameters = BoundConstructor.BoundConstructorParameters!;
+                    var boundProperties = new List<ModelMetadata>();
+
+                    foreach (var metadata in Properties)
+                    {
+                        if (!boundParameters.Any(p =>
+                            string.Equals(p.ParameterName, metadata.PropertyName, StringComparison.Ordinal)
+                            && p.ModelType == metadata.ModelType))
+                        {
+                            boundProperties.Add(metadata);
+                        }
+                    }
+
+                    _boundProperties = boundProperties;
+                }
+
+                return _boundProperties;
+            }
+        }
+
+        internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorParameterMapping
+        {
+            get
+            {
+                if (_parameterMapping != null)
+                {
+                    return _parameterMapping;
+                }
+
+                if (BoundConstructor is null)
+                {
+                    _parameterMapping = EmptyParameterMapping;
+                    return _parameterMapping;
+                }
+
+                var boundParameters = BoundConstructor.BoundConstructorParameters!;
+                var parameterMapping = new Dictionary<ModelMetadata, ModelMetadata>();
+
+                foreach (var parameter in boundParameters)
+                {
+                    var property = Properties.FirstOrDefault(p =>
+                        string.Equals(p.Name, parameter.ParameterName, StringComparison.Ordinal) &&
+                        p.ModelType == parameter.ModelType);
+
+                    if (property != null)
+                    {
+                        parameterMapping[parameter] = property;
+                    }
+                }
+
+                _parameterMapping = parameterMapping;
+                return _parameterMapping;
+            }
+        }
+
+        /// <summary>
+        /// Gets <see cref="ModelMetadata"/> instance for a constructor of a record type that is used during binding and validation.
+        /// </summary>
+        public virtual ModelMetadata? BoundConstructor { get; }
+
+        /// <summary>
+        /// Gets the collection of <see cref="ModelMetadata"/> instances for parameters on a <see cref="BoundConstructor"/>.
+        /// This is only available when <see cref="MetadataKind"/> is <see cref="ModelMetadataKind.Constructor"/>.
+        /// </summary>
+        public virtual IReadOnlyList<ModelMetadata>? BoundConstructorParameters { get; }
+
         /// <summary>
         /// Gets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
         /// </summary>
@@ -401,6 +489,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         /// </summary>
         public abstract Action<object, object> PropertySetter { get; }
 
+        /// <summary>
+        /// Gets a delegate that invokes the bound constructor <see cref="BoundConstructor" /> if non-<see langword="null" />.
+        /// </summary>
+        public virtual Func<object[], object>? BoundConstructorInvoker => null;
+
         /// <summary>
         /// Gets a display name for the model.
         /// </summary>
@@ -500,6 +593,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
                     return $"ModelMetadata (Property: '{ContainerType!.Name}.{PropertyName}' Type: '{ModelType.Name}')";
                 case ModelMetadataKind.Type:
                     return $"ModelMetadata (Type: '{ModelType.Name}')";
+                case ModelMetadataKind.Constructor:
+                    return $"ModelMetadata (Constructor: '{ModelType.Name}')";
                 default:
                     return $"Unsupported MetadataKind '{MetadataKind}'.";
             }

+ 12 - 1
src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadataProvider.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System;
@@ -54,5 +54,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
         {
             throw new NotSupportedException();
         }
+
+        /// <summary>
+        /// Supplies metadata describing a constructor.
+        /// </summary>
+        /// <param name="constructor">The <see cref="ConstructorInfo"/>.</param>
+        /// <param name="modelType">The type declaring the constructor.</param>
+        /// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="constructor"/>.</returns>
+        public virtual ModelMetadata GetMetadataForConstructor(ConstructorInfo constructor, Type modelType)
+        {
+            throw new NotSupportedException();
+        }
     }
 }

+ 1 - 0
src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs

@@ -298,6 +298,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
                 // "The value '' is not valid." (when no value was provided, not even an empty string) and
                 // "The supplied value is invalid for Int32." (when error is for an element or parameter).
                 var messageProvider = metadata.ModelBindingMessageProvider;
+
                 var name = metadata.DisplayName ?? metadata.PropertyName;
                 string errorMessage;
                 if (entry == null && name == null)

+ 7 - 7
src/Mvc/Mvc.Abstractions/test/ModelBinding/BindingInfoTest.cs

@@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             // Arrange
             var attributes = new object[]
             {
-                new ModelBinderAttribute { BinderType = typeof(ComplexTypeModelBinder), Name = "Test" },
+                new ModelBinderAttribute { BinderType = typeof(ComplexObjectModelBinder), Name = "Test" },
             };
             var modelType = typeof(Guid);
             var provider = new TestModelMetadataProvider();
@@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
 
             // Assert
             Assert.NotNull(bindingInfo);
-            Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
             Assert.Same("Test", bindingInfo.BinderModelName);
         }
 
@@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             // Arrange
             var attributes = new object[]
             {
-                new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
+                new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
                 new ControllerAttribute(),
                 new BindNeverAttribute(),
             };
@@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
 
             // Assert
             Assert.NotNull(bindingInfo);
-            Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
             Assert.Same("Different", bindingInfo.BinderModelName);
             Assert.Same(BindingSource.Custom, bindingInfo.BindingSource);
         }
@@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var provider = new TestModelMetadataProvider();
             provider.ForType(modelType).BindingDetails(metadata =>
             {
-                metadata.BinderType = typeof(ComplexTypeModelBinder);
+                metadata.BinderType = typeof(ComplexObjectModelBinder);
             });
             var modelMetadata = provider.GetMetadataForType(modelType);
 
@@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
 
             // Assert
             Assert.NotNull(bindingInfo);
-            Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
         }
 
         [Fact]
@@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             // Arrange
             var attributes = new object[]
             {
-                new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
+                new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
                 new ControllerAttribute(),
                 new BindNeverAttribute(),
             };

+ 6 - 0
src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelMetadataTest.cs

@@ -728,6 +728,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
                     throw new NotImplementedException();
                 }
             }
+
+            public override ModelMetadata BoundConstructor => throw new NotImplementedException();
+
+            public override Func<object[], object> BoundConstructorInvoker => throw new NotImplementedException();
+
+            public override IReadOnlyList<ModelMetadata> BoundConstructorParameters => throw new NotImplementedException();
         }
 
         private class CollectionImplementation : ICollection<string>

+ 1 - 1
src/Mvc/Mvc.Analyzers/test/TestFiles/TopLevelParameterNameAnalyzerTest/IsProblematicParameter_ReturnsTrue_IfPropertyWithModelBindingAttributeHasSameNameAsParameter.cs

@@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers.TopLevelParameterNameAnalyzerTestFi
 {
     public class IsProblematicParameter_ReturnsTrue_IfPropertyWithModelBindingAttributeHasSameNameAsParameter
     {
-        [ModelBinder(typeof(ComplexTypeModelBinder), Name = "model")]
+        [ModelBinder(typeof(ComplexObjectModelBinder), Name = "model")]
         public string Different { get; set; }
 
         public void ActionMethod(

+ 12 - 5
src/Mvc/Mvc.Core/src/BindAttribute.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
 
 namespace Microsoft.AspNetCore.Mvc
 {
@@ -56,17 +57,23 @@ namespace Microsoft.AspNetCore.Mvc
             {
                 if (Include != null && Include.Length > 0)
                 {
-                    if (_propertyFilter == null)
-                    {
-                        _propertyFilter = (m) => Include.Contains(m.PropertyName, StringComparer.Ordinal);
-                    }
-
+                    _propertyFilter ??= PropertyFilter;
                     return _propertyFilter;
                 }
                 else
                 {
                     return _default;
                 }
+
+                bool PropertyFilter(ModelMetadata modelMetadata)
+                {
+                    if (modelMetadata.MetadataKind == ModelMetadataKind.Parameter)
+                    {
+                        return Include.Contains(modelMetadata.ParameterName, StringComparer.Ordinal);
+                    }
+
+                    return Include.Contains(modelMetadata.PropertyName, StringComparer.Ordinal);
+                }
             }
         }
 

+ 1 - 1
src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs

@@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc
             options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
             options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
             options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
-            options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
+            options.ModelBinderProviders.Add(new ComplexObjectModelBinderProvider());
 
             // Set up filters
             options.Filters.Add(new UnsupportedContentTypeFilter());

+ 1 - 1
src/Mvc/Mvc.Core/src/Infrastructure/ParameterDefaultValues.cs

@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
 {
     internal static class ParameterDefaultValues
     {
-        public static object[] GetParameterDefaultValues(MethodInfo methodInfo)
+        public static object[] GetParameterDefaultValues(MethodBase methodInfo)
         {
             if (methodInfo == null)
             {

+ 752 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs

@@ -0,0 +1,752 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Core;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
+{
+    /// <summary>
+    /// <see cref="IModelBinder"/> implementation for binding complex types.
+    /// </summary>
+    public sealed class ComplexObjectModelBinder : IModelBinder
+    {
+        // Don't want a new public enum because communication between the private and internal methods of this class
+        // should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.
+
+        // Model contains only properties that are expected to bind from value providers and no value provider has
+        // matching data.
+        internal const int NoDataAvailable = 0;
+        // If model contains properties that are expected to bind from value providers, no value provider has matching
+        // data. Remaining (greedy) properties might bind successfully.
+        internal const int GreedyPropertiesMayHaveData = 1;
+        // Model contains at least one property that is expected to bind from value providers and a value provider has
+        // matching data.
+        internal const int ValueProviderDataAvailable = 2;
+
+        private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
+        private readonly IReadOnlyList<IModelBinder> _parameterBinders;
+        private readonly ILogger _logger;
+        private Func<object> _modelCreator;
+
+        internal ComplexObjectModelBinder(
+            IDictionary<ModelMetadata, IModelBinder> propertyBinders,
+            IReadOnlyList<IModelBinder> parameterBinders,
+            ILogger<ComplexObjectModelBinder> logger)
+        {
+            _propertyBinders = propertyBinders;
+            _parameterBinders = parameterBinders;
+            _logger = logger;
+        }
+
+        public Task BindModelAsync(ModelBindingContext bindingContext)
+        {
+            if (bindingContext == null)
+            {
+                throw new ArgumentNullException(nameof(bindingContext));
+            }
+
+            _logger.AttemptingToBindModel(bindingContext);
+
+            var parameterData = CanCreateModel(bindingContext);
+            if (parameterData == NoDataAvailable)
+            {
+                return Task.CompletedTask;
+            }
+
+            // Perf: separated to avoid allocating a state machine when we don't
+            // need to go async.
+            return BindModelCoreAsync(bindingContext, parameterData);
+        }
+
+        private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int propertyData)
+        {
+            Debug.Assert(propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable);
+
+            // Create model first (if necessary) to avoid reporting errors about properties when activation fails.
+            var attemptedBinding = false;
+            var bindingSucceeded = false;
+
+            var modelMetadata = bindingContext.ModelMetadata;
+
+            if (bindingContext.Model == null)
+            {
+                var boundConstructor = modelMetadata.BoundConstructor;
+                if (boundConstructor != null)
+                {
+                    var values = new object[boundConstructor.BoundConstructorParameters.Count];
+                    var (attemptedParameterBinding, parameterBindingSucceeded) = await BindParametersAsync(
+                        bindingContext,
+                        propertyData,
+                        boundConstructor.BoundConstructorParameters,
+                        values);
+
+                    attemptedBinding |= attemptedParameterBinding;
+                    bindingSucceeded |= parameterBindingSucceeded;
+
+                    if (!CreateModel(bindingContext, boundConstructor, values))
+                    {
+                        return;
+                    }
+                }
+                else
+                {
+                    CreateModel(bindingContext);
+                }
+            }
+
+            var (attemptedPropertyBinding, propertyBindingSucceeded) = await BindPropertiesAsync(
+                bindingContext,
+                propertyData,
+                modelMetadata.BoundProperties);
+
+            attemptedBinding |= attemptedPropertyBinding;
+            bindingSucceeded |= propertyBindingSucceeded;
+
+            // Have we created a top-level model despite an inability to bind anything in said model and a lack of
+            // other IsBindingRequired errors? Does that violate [BindRequired] on the model? This case occurs when
+            // 1. The top-level model has no public settable properties.
+            // 2. All properties in a [BindRequired] model have [BindNever] or are otherwise excluded from binding.
+            // 3. No data exists for any property.
+            if (!attemptedBinding &&
+                bindingContext.IsTopLevelObject &&
+                modelMetadata.IsBindingRequired)
+            {
+                var messageProvider = modelMetadata.ModelBindingMessageProvider;
+                var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
+                bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
+            }
+
+            _logger.DoneAttemptingToBindModel(bindingContext);
+
+            // Have all binders failed because no data was available?
+            //
+            // If CanCreateModel determined a property has data, failures are likely due to conversion errors. For
+            // example, user may submit ?[0].id=twenty&[1].id=twenty-one&[2].id=22 for a collection of a complex type
+            // with an int id property. In that case, the bound model should be [ {}, {}, { id = 22 }] and
+            // ModelState should contain errors about both [0].id and [1].id. Do not inform higher-level binders of the
+            // failure in this and similar cases.
+            //
+            // If CanCreateModel could not find data for non-greedy properties, failures indicate greedy binders were
+            // unsuccessful. For example, user may submit file attachments [0].File and [1].File but not [2].File for
+            // a collection of a complex type containing an IFormFile property. In that case, we have exhausted the
+            // attached files and checking for [3].File is likely be pointless. (And, if it had a point, would we stop
+            // after 10 failures, 100, or more -- all adding redundant errors to ModelState?) Inform higher-level
+            // binders of the failure.
+            //
+            // Required properties do not change the logic below. Missed required properties cause ModelState errors
+            // but do not necessarily prevent further attempts to bind.
+            //
+            // This logic is intended to maximize correctness but does not avoid infinite loops or recursion when a
+            // greedy model binder succeeds unconditionally.
+            if (!bindingContext.IsTopLevelObject &&
+                !bindingSucceeded &&
+                propertyData == GreedyPropertiesMayHaveData)
+            {
+                bindingContext.Result = ModelBindingResult.Failed();
+                return;
+            }
+
+            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
+        }
+
+        internal static bool CreateModel(ModelBindingContext bindingContext, ModelMetadata boundConstructor, object[] values)
+        {
+            try
+            {
+                bindingContext.Model = boundConstructor.BoundConstructorInvoker(values);
+                return true;
+            }
+            catch (Exception ex)
+            {
+                AddModelError(ex, bindingContext.ModelName, bindingContext);
+                bindingContext.Result = ModelBindingResult.Failed();
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Creates suitable <see cref="object"/> for given <paramref name="bindingContext"/>.
+        /// </summary>
+        /// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
+        /// <returns>An <see cref="object"/> compatible with <see cref="ModelBindingContext.ModelType"/>.</returns>
+        internal void CreateModel(ModelBindingContext bindingContext)
+        {
+            if (bindingContext == null)
+            {
+                throw new ArgumentNullException(nameof(bindingContext));
+            }
+
+            // If model creator throws an exception, we want to propagate it back up the call stack, since the
+            // application developer should know that this was an invalid type to try to bind to.
+            if (_modelCreator == null)
+            {
+                // The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
+                // reflection does not provide information about the implicit parameterless constructor for a struct.
+                // This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
+                // compile fails to construct it.
+                var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
+                if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
+                {
+                    var metadata = bindingContext.ModelMetadata;
+                    switch (metadata.MetadataKind)
+                    {
+                        case ModelMetadataKind.Parameter:
+                            throw new InvalidOperationException(
+                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
+                                    modelTypeInfo.FullName,
+                                    metadata.ParameterName));
+                        case ModelMetadataKind.Property:
+                            throw new InvalidOperationException(
+                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
+                                    modelTypeInfo.FullName,
+                                    metadata.PropertyName,
+                                    bindingContext.ModelMetadata.ContainerType.FullName));
+                        case ModelMetadataKind.Type:
+                            throw new InvalidOperationException(
+                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
+                                    modelTypeInfo.FullName));
+                    }
+                }
+
+                _modelCreator = Expression
+                    .Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
+                    .Compile();
+            }
+
+            bindingContext.Model = _modelCreator();
+        }
+
+        private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindParametersAsync(
+            ModelBindingContext bindingContext,
+            int propertyData,
+            IReadOnlyList<ModelMetadata> parameters,
+            object[] parameterValues)
+        {
+            var attemptedBinding = false;
+            var bindingSucceeded = false;
+
+            if (parameters.Count == 0)
+            {
+                return (attemptedBinding, bindingSucceeded);
+            }
+
+            var postponePlaceholderBinding = false;
+            for (var i = 0; i < parameters.Count; i++)
+            {
+                var parameter = parameters[i];
+                
+                var fieldName = parameter.BinderModelName ?? parameter.ParameterName;
+                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+
+                if (!CanBindItem(bindingContext, parameter))
+                {
+                    continue;
+                }
+
+                var parameterBinder = _parameterBinders[i];
+                if (parameterBinder is PlaceholderBinder)
+                {
+                    if (postponePlaceholderBinding)
+                    {
+                        // Decided to postpone binding properties that complete a loop in the model types when handling
+                        // an earlier loop-completing property. Postpone binding this property too.
+                        continue;
+                    }
+                    else if (!bindingContext.IsTopLevelObject &&
+                        !bindingSucceeded &&
+                        propertyData == GreedyPropertiesMayHaveData)
+                    {
+                        // Have no confirmation of data for the current instance. Postpone completing the loop until
+                        // we _know_ the current instance is useful. Recursion would otherwise occur prior to the
+                        // block with a similar condition after the loop.
+                        //
+                        // Example cases include an Employee class containing
+                        // 1. a Manager property of type Employee
+                        // 2. an Employees property of type IList<Employee>
+                        postponePlaceholderBinding = true;
+                        continue;
+                    }
+                }
+
+                var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);
+
+                if (result.IsModelSet)
+                {
+                    attemptedBinding = true;
+                    bindingSucceeded = true;
+
+                    parameterValues[i] = result.Model;
+                }
+                else if (parameter.IsBindingRequired)
+                {
+                    attemptedBinding = true;
+                }
+            }
+
+            if (postponePlaceholderBinding && bindingSucceeded)
+            {
+                // Have some data for this instance. Continue with the model type loop.
+                for (var i = 0; i < parameters.Count; i++)
+                {
+                    var parameter = parameters[i];
+                    if (!CanBindItem(bindingContext, parameter))
+                    {
+                        continue;
+                    }
+
+                    var parameterBinder = _parameterBinders[i];
+                    if (parameterBinder is PlaceholderBinder)
+                    {
+                        var fieldName = parameter.BinderModelName ?? parameter.ParameterName;
+                        var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+
+                        var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);
+
+                        if (result.IsModelSet)
+                        {
+                            parameterValues[i] = result.Model;
+                        }
+                    }
+                }
+            }
+
+            return (attemptedBinding, bindingSucceeded);
+        }
+
+        private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindPropertiesAsync(
+            ModelBindingContext bindingContext,
+            int propertyData,
+            IReadOnlyList<ModelMetadata> boundProperties)
+        {
+            var attemptedBinding = false;
+            var bindingSucceeded = false;
+
+            if (boundProperties.Count == 0)
+            {
+                return (attemptedBinding, bindingSucceeded);
+            }
+
+            var postponePlaceholderBinding = false;
+            for (var i = 0; i < boundProperties.Count; i++)
+            {
+                var property = boundProperties[i];
+                if (!CanBindItem(bindingContext, property))
+                {
+                    continue;
+                }
+
+                var propertyBinder = _propertyBinders[property];
+                if (propertyBinder is PlaceholderBinder)
+                {
+                    if (postponePlaceholderBinding)
+                    {
+                        // Decided to postpone binding properties that complete a loop in the model types when handling
+                        // an earlier loop-completing property. Postpone binding this property too.
+                        continue;
+                    }
+                    else if (!bindingContext.IsTopLevelObject &&
+                        !bindingSucceeded &&
+                        propertyData == GreedyPropertiesMayHaveData)
+                    {
+                        // Have no confirmation of data for the current instance. Postpone completing the loop until
+                        // we _know_ the current instance is useful. Recursion would otherwise occur prior to the
+                        // block with a similar condition after the loop.
+                        //
+                        // Example cases include an Employee class containing
+                        // 1. a Manager property of type Employee
+                        // 2. an Employees property of type IList<Employee>
+                        postponePlaceholderBinding = true;
+                        continue;
+                    }
+                }
+
+                var fieldName = property.BinderModelName ?? property.PropertyName;
+                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+                var result = await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);
+
+                if (result.IsModelSet)
+                {
+                    attemptedBinding = true;
+                    bindingSucceeded = true;
+                }
+                else if (property.IsBindingRequired)
+                {
+                    attemptedBinding = true;
+                }
+            }
+
+            if (postponePlaceholderBinding && bindingSucceeded)
+            {
+                // Have some data for this instance. Continue with the model type loop.
+                for (var i = 0; i < boundProperties.Count; i++)
+                {
+                    var property = boundProperties[i];
+                    if (!CanBindItem(bindingContext, property))
+                    {
+                        continue;
+                    }
+
+                    var propertyBinder = _propertyBinders[property];
+                    if (propertyBinder is PlaceholderBinder)
+                    {
+                        var fieldName = property.BinderModelName ?? property.PropertyName;
+                        var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+
+                        await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);
+                    }
+                }
+            }
+
+            return (attemptedBinding, bindingSucceeded);
+        }
+
+        internal bool CanBindItem(ModelBindingContext bindingContext, ModelMetadata propertyMetadata)
+        {
+            var metadataProviderFilter = bindingContext.ModelMetadata.PropertyFilterProvider?.PropertyFilter;
+            if (metadataProviderFilter?.Invoke(propertyMetadata) == false)
+            {
+                return false;
+            }
+
+            if (bindingContext.PropertyFilter?.Invoke(propertyMetadata) == false)
+            {
+                return false;
+            }
+
+            if (!propertyMetadata.IsBindingAllowed)
+            {
+                return false;
+            }
+
+            if (propertyMetadata.MetadataKind == ModelMetadataKind.Property && propertyMetadata.IsReadOnly)
+            {
+                // Determine if we can update a readonly property (such as a collection).
+                return CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
+            }
+
+            return true;
+        }
+
+        private async ValueTask<ModelBindingResult> BindPropertyAsync(
+            ModelBindingContext bindingContext,
+            ModelMetadata property,
+            IModelBinder propertyBinder,
+            string fieldName,
+            string modelName)
+        {
+            Debug.Assert(property.MetadataKind == ModelMetadataKind.Property);
+
+            // Pass complex (including collection) values down so that binding system does not unnecessarily
+            // recreate instances or overwrite inner properties that are not bound. No need for this with simple
+            // values because they will be overwritten if binding succeeds. Arrays are never reused because they
+            // cannot be resized.
+            object propertyModel = null;
+            if (property.PropertyGetter != null &&
+                property.IsComplexType &&
+                !property.ModelType.IsArray)
+            {
+                propertyModel = property.PropertyGetter(bindingContext.Model);
+            }
+
+            ModelBindingResult result;
+            using (bindingContext.EnterNestedScope(
+                modelMetadata: property,
+                fieldName: fieldName,
+                modelName: modelName,
+                model: propertyModel))
+            {
+                await propertyBinder.BindModelAsync(bindingContext);
+                result = bindingContext.Result;
+            }
+
+            if (result.IsModelSet)
+            {
+                SetProperty(bindingContext, modelName, property, result);
+            }
+            else if (property.IsBindingRequired)
+            {
+                var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
+                bindingContext.ModelState.TryAddModelError(modelName, message);
+            }
+
+            return result;
+        }
+
+        private async ValueTask<ModelBindingResult> BindParameterAsync(
+            ModelBindingContext bindingContext,
+            ModelMetadata parameter,
+            IModelBinder parameterBinder,
+            string fieldName,
+            string modelName)
+        {
+            Debug.Assert(parameter.MetadataKind == ModelMetadataKind.Parameter);
+
+            ModelBindingResult result;
+            using (bindingContext.EnterNestedScope(
+                modelMetadata: parameter,
+                fieldName: fieldName,
+                modelName: modelName,
+                model: null))
+            {
+                await parameterBinder.BindModelAsync(bindingContext);
+                result = bindingContext.Result;
+            }
+
+            if (!result.IsModelSet && parameter.IsBindingRequired)
+            {
+                var message = parameter.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
+                bindingContext.ModelState.TryAddModelError(modelName, message);
+            }
+
+            return result;
+        }
+
+        internal int CanCreateModel(ModelBindingContext bindingContext)
+        {
+            var isTopLevelObject = bindingContext.IsTopLevelObject;
+
+            // If we get here the model is a complex object which was not directly bound by any previous model binder,
+            // so we want to decide if we want to continue binding. This is important to get right to avoid infinite
+            // recursion.
+            //
+            // First, we want to make sure this object is allowed to come from a value provider source as this binder
+            // will only include value provider data. For instance if the model is marked with [FromBody], then we
+            // can just skip it. A greedy source cannot be a value provider.
+            //
+            // If the model isn't marked with ANY binding source, then we assume it's OK also.
+            //
+            // We skip this check if it is a top level object because we want to always evaluate
+            // the creation of top level object (this is also required for ModelBinderAttribute to work.)
+            var bindingSource = bindingContext.BindingSource;
+            if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
+            {
+                return NoDataAvailable;
+            }
+
+            // Create the object if:
+            // 1. It is a top level model.
+            if (isTopLevelObject)
+            {
+                return ValueProviderDataAvailable;
+            }
+
+            // 2. Any of the model properties can be bound.
+            return CanBindAnyModelItem(bindingContext);
+        }
+
+        private int CanBindAnyModelItem(ModelBindingContext bindingContext)
+        {
+            // If there are no properties on the model, and no constructor parameters, there is nothing to bind. We are here means this is not a top
+            // level object. So we return false.
+            var modelMetadata = bindingContext.ModelMetadata;
+            var performsConstructorBinding = bindingContext.Model == null && modelMetadata.BoundConstructor != null;
+
+            if (modelMetadata.Properties.Count == 0 &&
+                 (!performsConstructorBinding || modelMetadata.BoundConstructor.BoundConstructorParameters.Count == 0))
+            {
+                Log.NoPublicSettableItems(_logger, bindingContext);
+                return NoDataAvailable;
+            }
+
+            // We want to check to see if any of the properties of the model can be bound using the value providers or
+            // a greedy binder.
+            //
+            // Because a property might specify a custom binding source ([FromForm]), it's not correct
+            // for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
+            // that may include other value providers - that would lead us to mistakenly create the model
+            // when the data is coming from a source we should use (ex: value found in query string, but the
+            // model has [FromForm]).
+            //
+            // To do this we need to enumerate the properties, and see which of them provide a binding source
+            // through metadata, then we decide what to do.
+            //
+            //      If a property has a binding source, and it's a greedy source, then it's always bound.
+            //
+            //      If a property has a binding source, and it's a non-greedy source, then we'll filter the
+            //      the value providers to just that source, and see if we can find a matching prefix
+            //      (see CanBindValue).
+            //
+            //      If a property does not have a binding source, then it's fair game for any value provider.
+            //
+            // Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
+            // create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
+            // then we go ahead and create it.
+            var hasGreedyBinders = false;
+            for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
+            {
+                var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
+                if (!CanBindItem(bindingContext, propertyMetadata))
+                {
+                    continue;
+                }
+
+                // If any property can be bound from a greedy binding source, then success.
+                var bindingSource = propertyMetadata.BindingSource;
+                if (bindingSource != null && bindingSource.IsGreedy)
+                {
+                    hasGreedyBinders = true;
+                    continue;
+                }
+
+                // Otherwise, check whether the (perhaps filtered) value providers have a match.
+                var fieldName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
+                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+                using (bindingContext.EnterNestedScope(
+                    modelMetadata: propertyMetadata,
+                    fieldName: fieldName,
+                    modelName: modelName,
+                    model: null))
+                {
+                    // If any property can be bound from a value provider, then success.
+                    if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+                    {
+                        return ValueProviderDataAvailable;
+                    }
+                }
+            }
+
+            if (performsConstructorBinding)
+            {
+                var parameters = bindingContext.ModelMetadata.BoundConstructor.BoundConstructorParameters;
+                for (var i = 0; i < parameters.Count; i++)
+                {
+                    var parameterMetadata = parameters[i];
+                    if (!CanBindItem(bindingContext, parameterMetadata))
+                    {
+                        continue;
+                    }
+
+                    // If any parameter can be bound from a greedy binding source, then success.
+                    var bindingSource = parameterMetadata.BindingSource;
+                    if (bindingSource != null && bindingSource.IsGreedy)
+                    {
+                        hasGreedyBinders = true;
+                        continue;
+                    }
+
+                    // Otherwise, check whether the (perhaps filtered) value providers have a match.
+                    var fieldName = parameterMetadata.BinderModelName ?? parameterMetadata.ParameterName;
+                    var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
+                    using (bindingContext.EnterNestedScope(
+                        modelMetadata: parameterMetadata,
+                        fieldName: fieldName,
+                        modelName: modelName,
+                        model: null))
+                    {
+                        // If any parameter can be bound from a value provider, then success.
+                        if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+                        {
+                            return ValueProviderDataAvailable;
+                        }
+                    }
+                }
+            }
+
+            if (hasGreedyBinders)
+            {
+                return GreedyPropertiesMayHaveData;
+            }
+
+            _logger.CannotBindToComplexType(bindingContext);
+
+            return NoDataAvailable;
+        }
+
+        internal static bool CanUpdateReadOnlyProperty(Type propertyType)
+        {
+            // Value types have copy-by-value semantics, which prevents us from updating
+            // properties that are marked readonly.
+            if (propertyType.GetTypeInfo().IsValueType)
+            {
+                return false;
+            }
+
+            // Arrays are strange beasts since their contents are mutable but their sizes aren't.
+            // Therefore we shouldn't even try to update these. Further reading:
+            // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
+            if (propertyType.IsArray)
+            {
+                return false;
+            }
+
+            // Special-case known immutable reference types
+            if (propertyType == typeof(string))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        internal void SetProperty(
+            ModelBindingContext bindingContext,
+            string modelName,
+            ModelMetadata propertyMetadata,
+            ModelBindingResult result)
+        {
+            if (!result.IsModelSet)
+            {
+                // If we don't have a value, don't set it on the model and trounce a pre-initialized value.
+                return;
+            }
+
+            if (propertyMetadata.IsReadOnly)
+            {
+                // The property should have already been set when we called BindPropertyAsync, so there's
+                // nothing to do here.
+                return;
+            }
+
+            var value = result.Model;
+            try
+            {
+                propertyMetadata.PropertySetter(bindingContext.Model, value);
+            }
+            catch (Exception exception)
+            {
+                AddModelError(exception, modelName, bindingContext);
+            }
+        }
+
+        private static void AddModelError(
+            Exception exception,
+            string modelName,
+            ModelBindingContext bindingContext)
+        {
+            var targetInvocationException = exception as TargetInvocationException;
+            if (targetInvocationException?.InnerException != null)
+            {
+                exception = targetInvocationException.InnerException;
+            }
+
+            // Do not add an error message if a binding error has already occurred for this property.
+            var modelState = bindingContext.ModelState;
+            var validationState = modelState.GetFieldValidationState(modelName);
+            if (validationState == ModelValidationState.Unvalidated)
+            {
+                modelState.AddModelError(modelName, exception, bindingContext.ModelMetadata);
+            }
+        }
+
+        private static class Log
+        {
+            private static readonly Action<ILogger, string, Type, Exception> _noPublicSettableProperties = LoggerMessage.Define<string, Type>(
+               LogLevel.Debug,
+                new EventId(17, "NoPublicSettableItems"),
+               "Could not bind to model with name '{ModelName}' and type '{ModelType}' as the type has no public settable properties or constructor parameters.");
+
+            public static void NoPublicSettableItems(ILogger logger, ModelBindingContext bindingContext)
+            {
+                _noPublicSettableProperties(logger, bindingContext.ModelName, bindingContext.ModelType, null);
+            }
+        }
+    }
+}

+ 64 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinderProvider.cs

@@ -0,0 +1,64 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
+{
+    /// <summary>
+    /// An <see cref="IModelBinderProvider"/> for complex types.
+    /// </summary>
+    public class ComplexObjectModelBinderProvider : IModelBinderProvider
+    {
+        /// <inheritdoc />
+        public IModelBinder GetBinder(ModelBinderProviderContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            var metadata = context.Metadata;
+            if (metadata.IsComplexType && !metadata.IsCollectionType)
+            {
+                var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
+                var logger = loggerFactory.CreateLogger<ComplexObjectModelBinder>();
+                var parameterBinders = GetParameterBinders(context);
+
+                var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
+                for (var i = 0; i < context.Metadata.Properties.Count; i++)
+                {
+                    var property = context.Metadata.Properties[i];
+                    propertyBinders.Add(property, context.CreateBinder(property));
+                }
+
+                return new ComplexObjectModelBinder(propertyBinders, parameterBinders, logger);
+            }
+
+            return null;
+        }
+
+        private static IReadOnlyList<IModelBinder> GetParameterBinders(ModelBinderProviderContext context)
+        {
+            var boundConstructor = context.Metadata.BoundConstructor;
+            if (boundConstructor is null)
+            {
+                return Array.Empty<IModelBinder>();
+            }
+
+            var parameterBinders = boundConstructor.BoundConstructorParameters.Count == 0 ?
+                Array.Empty<IModelBinder>() :
+                new IModelBinder[boundConstructor.BoundConstructorParameters.Count];
+
+            for (var i = 0; i < parameterBinders.Length; i++)
+            {
+                parameterBinders[i] = context.CreateBinder(boundConstructor.BoundConstructorParameters[i]);
+            }
+
+            return parameterBinders;
+        }
+    }
+}

+ 1 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinder.cs

@@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
     /// <summary>
     /// <see cref="IModelBinder"/> implementation for binding complex types.
     /// </summary>
+    [Obsolete("This type is obsolete and will be removed in a future version. Use ComplexObjectModelBinder instead.")]
     public class ComplexTypeModelBinder : IModelBinder
     {
         // Don't want a new public enum because communication between the private and internal methods of this class

+ 2 - 1
src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexTypeModelBinderProvider.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System;
@@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
     /// <summary>
     /// An <see cref="IModelBinderProvider"/> for complex types.
     /// </summary>
+    [Obsolete("This type is obsolete and will be removed in a future version. Use ComplexObjectModelBinderProvider instead.")]
     public class ComplexTypeModelBinderProvider : IModelBinderProvider
     {
         /// <inheritdoc />

+ 6 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/BindingMetadata.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Reflection;
 using Microsoft.AspNetCore.Mvc.Core;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@@ -97,5 +98,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// See <see cref="ModelMetadata.PropertyFilterProvider"/>.
         /// </summary>
         public IPropertyFilterProvider PropertyFilterProvider { get; set; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="ConstructorInfo"/> used to model bind and validate the model type.
+        /// </summary>
+        public ConstructorInfo BoundConstructor { get; set; }
     }
 }

+ 74 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultBindingMetadataProvider.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
+using Microsoft.Extensions.Internal;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 {
@@ -72,6 +73,79 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
                 context.BindingMetadata.IsBindingAllowed = bindingBehavior.Behavior != BindingBehavior.Never;
                 context.BindingMetadata.IsBindingRequired = bindingBehavior.Behavior == BindingBehavior.Required;
             }
+
+            if (GetBoundConstructor(context.Key.ModelType) is ConstructorInfo constructorInfo)
+            {
+                context.BindingMetadata.BoundConstructor = constructorInfo;
+            }
+        }
+
+        internal static ConstructorInfo GetBoundConstructor(Type type)
+        {
+            if (type.IsAbstract || type.IsValueType || type.IsInterface)
+            {
+                return null;
+            }
+
+            var constructors = type.GetConstructors();
+            if (constructors.Length == 0)
+            {
+                return null;
+            }
+
+            return GetRecordTypeConstructor(type, constructors);
+        }
+
+        private static ConstructorInfo GetRecordTypeConstructor(Type type, ConstructorInfo[] constructors)
+        {
+            if (!IsRecordType(type))
+            {
+                return null;
+            }
+
+            // For record types, we will support binding and validating the primary constructor.
+            // There isn't metadata to identify a primary constructor. Our heuristic is:
+            // We require exactly one constructor to be defined on the type, and that every parameter on
+            // that constructor is mapped to a property with the same name and type.
+
+            if (constructors.Length > 1)
+            {
+                return null;
+            }
+
+            var constructor = constructors[0];
+
+            var parameters = constructor.GetParameters();
+            if (parameters.Length == 0)
+            {
+                // We do not need to do special handling for parameterless constructors.
+                return null;
+            }
+
+            var properties = PropertyHelper.GetVisibleProperties(type);
+
+            for (var i = 0; i < parameters.Length; i++)
+            {
+                var parameter = parameters[i];
+                var mappedProperty = properties.FirstOrDefault(property =>
+                    string.Equals(property.Name, parameter.Name, StringComparison.Ordinal) &&
+                    property.Property.PropertyType == parameter.ParameterType);
+
+                if (mappedProperty is null)
+                {
+                    // No property found, this is not a primary constructor.
+                    return null;
+                }
+            }
+
+            return constructor;
+
+            static bool IsRecordType(Type type)
+            {
+                // Based on the state of the art as described in https://github.com/dotnet/roslyn/issues/45777
+                var cloneMethod = type.GetMethod("<>Clone", BindingFlags.Public | BindingFlags.Instance);
+                return cloneMethod != null && cloneMethod.ReturnType == type;
+            }
         }
 
         private static BindingBehaviorAttribute FindBindingBehavior(BindingMetadataProviderContext context)

+ 11 - 1
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultMetadataDetails.cs

@@ -54,6 +54,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// </summary>
         public ModelMetadata[] Properties { get; set; }
 
+        /// <summary>
+        /// Gets or sets the <see cref="ModelMetadata"/> entries for constructor parameters.
+        /// </summary>
+        public ModelMetadata[] BoundConstructorParameters { get; set; }
+
         /// <summary>
         /// Gets or sets a property getter delegate to get the property value from a model object.
         /// </summary>
@@ -64,6 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// </summary>
         public Action<object, object> PropertySetter { get; set; }
 
+        /// <summary>
+        /// Gets or sets a delegate used to invoke the bound constructor for record types.
+        /// </summary>
+        public Func<object[], object> BoundConstructorInvoker { get; set; }
+
         /// <summary>
         /// Gets or sets the <see cref="Metadata.ValidationMetadata"/>
         /// </summary>
@@ -74,4 +84,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// </summary>
         public ModelMetadata ContainerMetadata { get; set; }
     }
-}
+}

+ 37 - 1
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultModelMetadata.cs

@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 
         private ReadOnlyDictionary<object, object> _additionalValues;
         private ModelMetadata _elementMetadata;
+        private ModelMetadata _constructorMetadata;
         private bool? _isBindingRequired;
         private bool? _isReadOnly;
         private bool? _isRequired;
@@ -386,6 +387,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             }
         }
 
+        /// <inheritdoc />
+        public override ModelMetadata BoundConstructor
+        {
+            get
+            {
+                if (BindingMetadata.BoundConstructor == null)
+                {
+                    return null;
+                }
+
+                if (_constructorMetadata == null)
+                {
+                    var modelMetadataProvider = (ModelMetadataProvider)_provider;
+                    _constructorMetadata = modelMetadataProvider.GetMetadataForConstructor(BindingMetadata.BoundConstructor, ModelType);
+                }
+
+                return _constructorMetadata;
+            }
+        }
+
+        public override IReadOnlyList<ModelMetadata> BoundConstructorParameters => _details.BoundConstructorParameters;
+
         /// <inheritdoc />
         public override IPropertyFilterProvider PropertyFilterProvider => BindingMetadata.PropertyFilterProvider;
 
@@ -494,7 +517,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             }
             else if (defaultModelMetadata.IsComplexType)
             {
-                foreach (var property in defaultModelMetadata.Properties)
+                var parameters = defaultModelMetadata.BoundConstructor?.BoundConstructorParameters ?? Array.Empty<ModelMetadata>();
+                foreach (var parameter in parameters)
+                {
+                    if (CalculateHasValidators(visited, parameter))
+                    {
+                        return true;
+                    }
+                }
+
+                foreach (var property in defaultModelMetadata.BoundProperties)
                 {
                     if (CalculateHasValidators(visited, property))
                     {
@@ -527,6 +559,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         /// <inheritdoc />
         public override Action<object, object> PropertySetter => _details.PropertySetter;
 
+        public override Func<object[], object> BoundConstructorInvoker => _details.BoundConstructorInvoker;
+
+        internal DefaultMetadataDetails Details => _details;
+
         /// <inheritdoc />
         public override ModelMetadata GetMetadataForType(Type modelType)
         {

+ 99 - 7
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultModelMetadataProvider.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Linq.Expressions;
 using System.Reflection;
 using Microsoft.Extensions.Internal;
 using Microsoft.Extensions.Options;
@@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
     /// </summary>
     public class DefaultModelMetadataProvider : ModelMetadataProvider
     {
-        private readonly TypeCache _typeCache = new TypeCache();
+        private readonly ModelMetadataCache _modelMetadataCache = new ModelMetadataCache();
         private readonly Func<ModelMetadataIdentity, ModelMetadataCacheEntry> _cacheEntryFactory;
         private readonly ModelMetadataCacheEntry _metadataCacheEntryForObjectType;
 
@@ -150,6 +151,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 
             return cacheEntry.Metadata;
         }
+        
+        /// <inheritdoc />
+        public override ModelMetadata GetMetadataForConstructor(ConstructorInfo constructorInfo, Type modelType)
+        {
+            if (constructorInfo is null)
+            {
+                throw new ArgumentNullException(nameof(constructorInfo));
+            }
+
+            var cacheEntry = GetCacheEntry(constructorInfo, modelType);
+            return cacheEntry.Metadata;
+        }
 
         private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions<MvcOptions> optionsAccessor)
         {
@@ -174,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             {
                 var key = ModelMetadataIdentity.ForType(modelType);
 
-                cacheEntry = _typeCache.GetOrAdd(key, _cacheEntryFactory);
+                cacheEntry = _modelMetadataCache.GetOrAdd(key, _cacheEntryFactory);
             }
 
             return cacheEntry;
@@ -182,22 +195,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 
         private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType)
         {
-            return _typeCache.GetOrAdd(
+            return _modelMetadataCache.GetOrAdd(
                 ModelMetadataIdentity.ForParameter(parameter, modelType),
                 _cacheEntryFactory);
         }
 
         private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType)
         {
-            return _typeCache.GetOrAdd(
+            return _modelMetadataCache.GetOrAdd(
                 ModelMetadataIdentity.ForProperty(property, modelType, property.DeclaringType),
                 _cacheEntryFactory);
         }
 
+        private ModelMetadataCacheEntry GetCacheEntry(ConstructorInfo constructor, Type modelType)
+        {
+            return _modelMetadataCache.GetOrAdd(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType),
+                _cacheEntryFactory);
+        }
+
         private ModelMetadataCacheEntry CreateCacheEntry(ModelMetadataIdentity key)
         {
             DefaultMetadataDetails details;
-            if (key.MetadataKind == ModelMetadataKind.Parameter)
+
+            if (key.MetadataKind == ModelMetadataKind.Constructor)
+            {
+                details = CreateConstructorDetails(key);
+            }
+            else if (key.MetadataKind == ModelMetadataKind.Parameter)
             {
                 details = CreateParameterDetails(key);
             }
@@ -230,6 +255,73 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             return null;
         }
 
+        private DefaultMetadataDetails CreateConstructorDetails(ModelMetadataIdentity constructorKey)
+        {
+            var constructor = constructorKey.ConstructorInfo;
+            var parameters = constructor.GetParameters();
+            var parameterMetadata = new ModelMetadata[parameters.Length];
+            var parameterTypes = new Type[parameters.Length];
+
+            for (var i = 0; i < parameters.Length; i++)
+            {
+                var parameter = parameters[i];
+                var parameterDetails = CreateParameterDetails(ModelMetadataIdentity.ForParameter(parameter));
+                parameterMetadata[i] = CreateModelMetadata(parameterDetails);
+
+                parameterTypes[i] = parameter.ParameterType;
+            }
+
+            var constructorDetails = new DefaultMetadataDetails(constructorKey, ModelAttributes.Empty);
+            constructorDetails.BoundConstructorParameters = parameterMetadata;
+            constructorDetails.BoundConstructorInvoker = CreateObjectFactory(constructor);
+
+            return constructorDetails;
+
+            static Func<object[], object> CreateObjectFactory(ConstructorInfo constructor)
+            {
+                var args = Expression.Parameter(typeof(object[]), "args");
+                var factoryExpressionBody = BuildFactoryExpression(constructor, args);
+
+                var factoryLamda = Expression.Lambda<Func<object[], object>>(factoryExpressionBody, args);
+
+                return factoryLamda.Compile();
+            }
+        }
+
+        private static Expression BuildFactoryExpression(
+            ConstructorInfo constructor,
+            Expression factoryArgumentArray)
+        {
+            var constructorParameters = constructor.GetParameters();
+            var constructorArguments = new Expression[constructorParameters.Length];
+
+            for (var i = 0; i < constructorParameters.Length; i++)
+            {
+                var constructorParameter = constructorParameters[i];
+                var parameterType = constructorParameter.ParameterType;
+
+                constructorArguments[i] = Expression.ArrayAccess(factoryArgumentArray, Expression.Constant(i));
+                if (ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out var defaultValue))
+                {
+                    // We have a default value;
+                }
+                else if (parameterType.IsValueType)
+                {
+                    defaultValue = Activator.CreateInstance(parameterType);
+                }
+
+                if (defaultValue != null)
+                {
+                    var defaultValueExpression = Expression.Constant(defaultValue);
+                    constructorArguments[i] = Expression.Coalesce(constructorArguments[i], defaultValueExpression);
+                }
+
+                constructorArguments[i] = Expression.Convert(constructorArguments[i], parameterType);
+            }
+
+            return Expression.New(constructor, constructorArguments);
+        }
+
         private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType()
         {
             var key = ModelMetadataIdentity.ForType(typeof(object));
@@ -341,7 +433,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
                 ModelAttributes.GetAttributesForParameter(key.ParameterInfo, key.ModelType));
         }
 
-        private class TypeCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry>
+        private class ModelMetadataCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry>
         {
         }
 
@@ -358,4 +450,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             public DefaultMetadataDetails Details { get; }
         }
     }
-}
+}

+ 5 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultValidationMetadataProvider.cs

@@ -53,6 +53,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 
                 context.ValidationMetadata.PropertyValidationFilter = validationFilter;
             }
+            else if (context.Key.MetadataKind == ModelMetadataKind.Parameter)
+            {
+                var validationFilter = context.ParameterAttributes.OfType<IPropertyValidationFilter>().FirstOrDefault();
+                context.ValidationMetadata.PropertyValidationFilter = validationFilter;
+            }
         }
     }
 }

+ 10 - 0
src/Mvc/Mvc.Core/src/ModelBinding/Metadata/ModelAttributes.cs

@@ -13,6 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
     /// </summary>
     public class ModelAttributes
     {
+        internal static readonly ModelAttributes Empty = new ModelAttributes(Array.Empty<object>());
+
+        /// <summary>
+        /// Creates a new <see cref="ModelAttributes"/>.
+        /// </summary>
+        internal ModelAttributes(IReadOnlyList<object> attributes)
+        {
+            Attributes = attributes;
+        }
+
         /// <summary>
         /// Creates a new <see cref="ModelAttributes"/>.
         /// </summary>

+ 1 - 1
src/Mvc/Mvc.Core/src/ModelBinding/ParameterBinder.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System;

+ 54 - 35
src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs

@@ -4,7 +4,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Reflection;
+using Microsoft.AspNetCore.Mvc.Core;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
 {
@@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
     /// </summary>
     internal class DefaultComplexObjectValidationStrategy : IValidationStrategy
     {
-        private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null;
-
         /// <summary>
         /// Gets an instance of <see cref="DefaultComplexObjectValidationStrategy"/>.
         /// </summary>
@@ -30,27 +28,42 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
             string key,
             object model)
         {
-            return new Enumerator(metadata.Properties, key, model);
+            return new Enumerator(metadata, key, model);
         }
 
         private class Enumerator : IEnumerator<ValidationEntry>
         {
             private readonly string _key;
             private readonly object _model;
-            private readonly ModelPropertyCollection _properties;
+            private readonly int _count;
+            private readonly ModelMetadata _modelMetadata;
+            private readonly IReadOnlyList<ModelMetadata> _parameters;
+            private readonly IReadOnlyList<ModelMetadata> _properties;
 
             private ValidationEntry _entry;
             private int _index;
 
             public Enumerator(
-                ModelPropertyCollection properties,
+                ModelMetadata modelMetadata,
                 string key,
                 object model)
             {
-                _properties = properties;
+                _modelMetadata = modelMetadata;
                 _key = key;
                 _model = model;
 
+                if (_modelMetadata.BoundConstructor == null)
+                {
+                    _parameters = Array.Empty<ModelMetadata>();
+                }
+                else
+                {
+                    _parameters = _modelMetadata.BoundConstructor.BoundConstructorParameters;
+                }
+
+                _properties = _modelMetadata.BoundProperties;
+                _count = _properties.Count + _parameters.Count;
+
                 _index = -1;
             }
 
@@ -61,27 +74,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
             public bool MoveNext()
             {
                 _index++;
-                if (_index >= _properties.Count)
+                
+                if (_index >= _count)
                 {
                     return false;
                 }
 
-                var property = _properties[_index];
-                var propertyName = property.BinderModelName ?? property.PropertyName;
-                var key = ModelNames.CreatePropertyModelName(_key, propertyName);
-
-                if (_model == null)
+                if (_index < _parameters.Count)
                 {
-                    // Performance: Never create a delegate when container is null.
-                    _entry = new ValidationEntry(property, key, model: null);
-                }
-                else if (IsMono)
-                {
-                    _entry = new ValidationEntry(property, key, () => GetModelOnMono(_model, property.PropertyName));
+                    var parameter = _parameters[_index];
+                    var parameterName = parameter.BinderModelName ?? parameter.ParameterName;
+                    var key = ModelNames.CreatePropertyModelName(_key, parameterName);
+
+                    if (_model is null)
+                    {
+                        _entry = new ValidationEntry(parameter, key, model: null);
+                    }
+                    else
+                    {
+                        if (!_modelMetadata.BoundConstructorParameterMapping.TryGetValue(parameter, out var property))
+                        {
+                            throw new InvalidOperationException(
+                                Resources.FormatValidationStrategy_MappedPropertyNotFound(parameter, _modelMetadata.ModelType));
+                        }
+
+                        _entry = new ValidationEntry(parameter, key, () => GetModel(_model, property));
+                    }
                 }
                 else
                 {
-                    _entry = new ValidationEntry(property, key, () => GetModel(_model, property));
+                    var property = _properties[_index - _parameters.Count];
+                    var propertyName = property.BinderModelName ?? property.PropertyName;
+                    var key = ModelNames.CreatePropertyModelName(_key, propertyName);
+
+                    if (_model == null)
+                    {
+                        // Performance: Never create a delegate when container is null.
+                        _entry = new ValidationEntry(property, key, model: null);
+                    }
+                    else
+                    {
+                        _entry = new ValidationEntry(property, key, () => GetModel(_model, property));
+                    }
                 }
 
                 return true;
@@ -100,21 +134,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
             {
                 return property.PropertyGetter(container);
             }
-
-            // Our property accessors don't work on Mono 4.0.4 - see https://github.com/aspnet/External/issues/44
-            // This is a workaround for what the PropertyGetter does in the background.
-            private static object GetModelOnMono(object container, string propertyName)
-            {
-                var propertyInfo = container.GetType().GetRuntimeProperty(propertyName);
-                try
-                {
-                    return propertyInfo.GetValue(container);
-                }
-                catch (TargetInvocationException ex)
-                {
-                    throw ex.InnerException;
-                }
-            }
         }
     }
 }

+ 6 - 5
src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidateNeverAttribute.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System;
@@ -6,11 +6,12 @@ using System;
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
 {
     /// <summary>
-    /// <see cref="IPropertyValidationFilter"/> implementation that unconditionally indicates a property should be
-    /// excluded from validation. When applied to a property, the validation system excludes that property. When
-    /// applied to a type, the validation system excludes all properties within that type.
+    /// Indicates that a property or parameter should be excluded from validation.
+    /// When applied to a property, the validation system excludes that property.
+    /// When applied to a parameter, the validation system excludes that parameter.
+    /// When applied to a type, the validation system excludes all properties within that type.
     /// </summary>
-    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
     public sealed class ValidateNeverAttribute : Attribute, IPropertyValidationFilter
     {
         /// <inheritdoc />

+ 12 - 0
src/Mvc/Mvc.Core/src/Resources.resx

@@ -522,4 +522,16 @@
   <data name="StateShouldBeNullForRouteValueTransformers" xml:space="preserve">
     <value>Transformer '{0}' was retrieved from dependency injection with a state value. State can only be specified when the dynamic route is mapped using MapDynamicControllerRoute's state argument together with transient lifetime transformer. Ensure that '{0}' doesn't set its own state and that the transformer is registered with a transient lifetime in dependency injection.</value>
   </data>
+  <data name="ComplexObjectModelBinder_NoSuitableConstructor_ForParameter" xml:space="preserve">
+    <value>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. Record types must have a single primary constructor. Alternatively, give the '{1}' parameter a non-null default value.</value>
+  </data>
+  <data name="ComplexObjectModelBinder_NoSuitableConstructor_ForProperty" xml:space="preserve">
+    <value>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. Record types must have a single primary constructor. Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor.</value>
+  </data>
+  <data name="ComplexObjectModelBinder_NoSuitableConstructor_ForType" xml:space="preserve">
+    <value>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. Record types must have a single primary constructor.</value>
+  </data>
+  <data name="ValidationStrategy_MappedPropertyNotFound" xml:space="preserve">
+    <value>No property found that maps to constructor parameter '{0}' for type '{1}'. Validation requires that each bound parameter of a record type's primary constructor must have a property to read the value.</value>
+  </data>
 </root>

+ 2 - 2
src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs

@@ -1270,7 +1270,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
         {
             public string Property { get; set; }
 
-            [ModelBinder(typeof(ComplexTypeModelBinder))]
+            [ModelBinder(typeof(ComplexObjectModelBinder))]
             public string BinderType { get; set; }
 
             [FromRoute]
@@ -1307,7 +1307,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
 
             // Assert
             var bindingInfo = property.BindingInfo;
-            Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
         }
 
         [Fact]

+ 1 - 1
src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs

@@ -998,7 +998,7 @@ Environment.NewLine + "int b";
         private class ParameterWithBindingInfo
         {
             [HttpGet("test")]
-            public IActionResult Action([ModelBinder(typeof(ComplexTypeModelBinder))] Car car) => null;
+            public IActionResult Action([ModelBinder(typeof(ComplexObjectModelBinder))] Car car) => null;
         }
     }
 }

+ 2 - 1
src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj

@@ -1,8 +1,9 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
     <RootNamespace>Microsoft.AspNetCore.Mvc</RootNamespace>
+    <LangVersion>9.0</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>

+ 92 - 0
src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexObjectModelBinderProviderTest.cs

@@ -0,0 +1,92 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
+{
+    public class ComplexObjectModelBinderProviderTest
+    {
+        [Theory]
+        [InlineData(typeof(string))]
+        [InlineData(typeof(int))]
+        [InlineData(typeof(List<int>))]
+        public void Create_ForNonComplexType_ReturnsNull(Type modelType)
+        {
+            // Arrange
+            var provider = new ComplexObjectModelBinderProvider();
+
+            var context = new TestModelBinderProviderContext(modelType);
+
+            // Act
+            var result = provider.GetBinder(context);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        [Fact]
+        public void Create_ForSupportedTypes_ReturnsBinder()
+        {
+            // Arrange
+            var provider = new ComplexObjectModelBinderProvider();
+
+            var context = new TestModelBinderProviderContext(typeof(Person));
+            context.OnCreatingBinder(m =>
+            {
+                if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
+                {
+                    return Mock.Of<IModelBinder>();
+                }
+                else
+                {
+                    Assert.False(true, "Not the right model type");
+                    return null;
+                }
+            });
+
+            // Act
+            var result = provider.GetBinder(context);
+
+            // Assert
+            Assert.IsType<ComplexObjectModelBinder>(result);
+        }
+
+        [Fact]
+        public void Create_ForSupportedType_ReturnsBinder()
+        {
+            // Arrange
+            var provider = new ComplexObjectModelBinderProvider();
+
+            var context = new TestModelBinderProviderContext(typeof(Person));
+            context.OnCreatingBinder(m =>
+            {
+                if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
+                {
+                    return Mock.Of<IModelBinder>();
+                }
+                else
+                {
+                    Assert.False(true, "Not the right model type");
+                    return null;
+                }
+            });
+
+            // Act
+            var result = provider.GetBinder(context);
+
+            // Assert
+            Assert.IsType<ComplexObjectModelBinder>(result);
+        }
+
+        private class Person
+        {
+            public string Name { get; set; }
+
+            public int Age { get; set; }
+        }
+    }
+}

+ 1412 - 0
src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexObjectModelBinderTest.cs

@@ -0,0 +1,1412 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using Castle.Core.Logging;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
+{
+    public class ComplexObjectModelBinderTest
+    {
+        private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+        private readonly ILogger<ComplexObjectModelBinder> _logger = NullLogger<ComplexObjectModelBinder>.Instance;
+
+        [Theory]
+        [InlineData(true, ComplexObjectModelBinder.ValueProviderDataAvailable)]
+        [InlineData(false, ComplexObjectModelBinder.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(ComplexObjectModelBinder.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(ComplexObjectModelBinder.ValueProviderDataAvailable, canCreate);
+        }
+
+        [Theory]
+        [InlineData(ComplexObjectModelBinder.ValueProviderDataAvailable)]
+        [InlineData(ComplexObjectModelBinder.GreedyPropertiesMayHaveData)]
+        public void CanCreateModel_CreatesModel_WithAllGreedyProperties(int expectedCanCreate)
+        {
+            var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties)));
+            bindingContext.IsTopLevelObject = expectedCanCreate == ComplexObjectModelBinder.ValueProviderDataAvailable;
+
+            var binder = CreateBinder(bindingContext.ModelMetadata);
+
+            // Act
+            var canCreate = binder.CanCreateModel(bindingContext);
+
+            // Assert
+            Assert.Equal(expectedCanCreate, canCreate);
+        }
+
+        [Theory]
+        [InlineData(ComplexObjectModelBinder.ValueProviderDataAvailable)]
+        [InlineData(ComplexObjectModelBinder.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 == ComplexObjectModelBinder.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(ComplexObjectModelBinder.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(ComplexObjectModelBinder.ValueProviderDataAvailable, canCreate);
+        }
+
+        [Theory]
+        [InlineData(typeof(TypeWithNoBinderMetadata), ComplexObjectModelBinder.NoDataAvailable)]
+        [InlineData(typeof(TypeWithNoBinderMetadata), ComplexObjectModelBinder.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 == ComplexObjectModelBinder.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), ComplexObjectModelBinder.GreedyPropertiesMayHaveData)]
+        [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexObjectModelBinder.ValueProviderDataAvailable)]
+        [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexObjectModelBinder.GreedyPropertiesMayHaveData)]
+        [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexObjectModelBinder.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 == ComplexObjectModelBinder.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), ComplexObjectModelBinder.GreedyPropertiesMayHaveData)]
+        [InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexObjectModelBinder.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 == ComplexObjectModelBinder.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, ComplexObjectModelBinder.GreedyPropertiesMayHaveData)]
+        [InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true, ComplexObjectModelBinder.ValueProviderDataAvailable)]
+        [InlineData(typeof(TypeWithNoBinderMetadata), false, ComplexObjectModelBinder.NoDataAvailable)]
+        [InlineData(typeof(TypeWithNoBinderMetadata), true, ComplexObjectModelBinder.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;
+
+        [Fact]
+        public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoData()
+        {
+            // Arrange
+            var parameter = typeof(ComplexObjectModelBinderTest)
+                .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 ComplexObjectModelBinder(
+                binders,
+                Array.Empty<IModelBinder>(),
+                _logger);
+
+            // 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(ComplexObjectModelBinderTest)
+                .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 ComplexObjectModelBinder(
+                new Dictionary<ModelMetadata, IModelBinder>(),
+                Array.Empty<IModelBinder>(),
+                _logger);
+
+            // 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(ComplexObjectModelBinderTest)
+                .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 ComplexObjectModelBinder(
+                new Dictionary<ModelMetadata, IModelBinder>(),
+                Array.Empty<IModelBinder>(),
+                _logger);
+
+            // 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(CollectionContainer.ReadOnlyArray), false)]
+        [InlineData(nameof(CollectionContainer.ReadOnlyDictionary), true)]
+        [InlineData(nameof(CollectionContainer.ReadOnlyList), 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 = ComplexObjectModelBinder.CanUpdateReadOnlyProperty(metadata.ModelType);
+
+            // Assert
+            Assert.Equal(expected, canUpdate);
+        }
+
+        private class PersonWithName
+        {
+            public string Name { get; set; }
+        }
+
+        [Fact]
+        public async Task BindModelAsync_ModelIsNotNull_DoesNotCallCreateModel()
+        {
+            // Arrange
+            var bindingContext = CreateContext(GetMetadataForType(typeof(PersonWithName)), new PersonWithName());
+            var originalModel = bindingContext.Model;
+
+            var binders = bindingContext.ModelMetadata.Properties.ToDictionary(
+                keySelector: item => item,
+                elementSelector: item => (IModelBinder)new TestModelBinderProvider(item, ModelBindingResult.Success("Test")));
+
+            var binder = new ComplexObjectModelBinder(
+                binders,
+                Array.Empty<IModelBinder>(),
+                _logger);
+
+            // Act
+            await binder.BindModelAsync(bindingContext);
+
+            // Assert
+            Assert.Same(originalModel, bindingContext.Model);
+        }
+
+        [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.CanBindItem(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.CanBindItem(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.CanBindItem(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.CanBindItem(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.CanBindItem(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 propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Failed());
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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);
+        }
+
+        private class TestModelBinderProvider : IModelBinderProvider, IModelBinder
+        {
+            private readonly ModelMetadata _modelMetadata;
+            private readonly ModelBindingResult _result;
+
+            public TestModelBinderProvider(ModelMetadata modelMetadata, ModelBindingResult result)
+            {
+                _modelMetadata = modelMetadata;
+                _result = result;
+            }
+
+            public Task BindModelAsync(ModelBindingContext bindingContext)
+            {
+                bindingContext.Result = _result;
+                return Task.CompletedTask;
+            }
+
+            public IModelBinder GetBinder(ModelBinderProviderContext context)
+            {
+                if (context.Metadata == _modelMetadata)
+                {
+                    return this;
+                }
+
+                return null;
+            }
+        }
+
+        [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 property = GetMetadataForProperty(model.GetType(), nameof(ModelWithDataMemberIsRequired.Age));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Failed());
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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);
+
+            // 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));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Success(model: null));
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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 property = GetMetadataForProperty(model.GetType(), nameof(BindingOptionalProperty.ValueTypeRequired));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Failed());
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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 property = GetMetadataForProperty(model.GetType(), nameof(NullableValueTypeProperty.NullableValueType));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Failed());
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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 property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Failed());
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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 property = GetMetadataForProperty(model.GetType(), nameof(Person.ValueTypeRequired));
+            var propertyBinder = new TestModelBinderProvider(property, ModelBindingResult.Success(model: 57));
+
+            var binder = CreateBinder(bindingContext.ModelMetadata, options =>
+            {
+                options.ModelBinderProviders.Insert(0, propertyBinder);
+            });
+
+            // 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, options =>
+            {
+                var firstNameProperty = containerMetadata.Properties[nameof(model.FirstName)];
+                options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(firstNameProperty, ModelBindingResult.Success("John")));
+
+                var lastNameProperty = containerMetadata.Properties[nameof(model.LastName)];
+                options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(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);
+        }
+
+        [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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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.SetProperty(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 ComplexObjectModelBinder CreateBinder(ModelMetadata metadata, Action<MvcOptions> configureOptions = null)
+        {
+            var options = Options.Create(new MvcOptions());
+            var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory());
+            setup.Configure(options.Value);
+
+            configureOptions?.Invoke(options.Value);
+
+            var factory = TestModelBinderFactory.Create(options.Value.ModelBinderProviders.ToArray());
+            return (ComplexObjectModelBinder)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 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 };
+        }
+    }
+}

+ 3 - 1
src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderProviderTest.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System;
@@ -8,6 +8,7 @@ using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
 {
+#pragma warning disable CS0618 // Type or member is obsolete
     public class ComplexTypeModelBinderProviderTest
     {
         [Theory]
@@ -89,4 +90,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
             public int Age { get; set; }
         }
     }
+#pragma warning restore CS0618 // Type or member is obsolete
 }

+ 3 - 2
src/Mvc/Mvc.Core/test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs

@@ -19,6 +19,7 @@ using Xunit;
 
 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();
@@ -1229,8 +1230,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
             setup.Configure(options.Value);
 
             var lastIndex = options.Value.ModelBinderProviders.Count - 1;
-            Assert.IsType<ComplexTypeModelBinderProvider>(options.Value.ModelBinderProviders[lastIndex]);
-            options.Value.ModelBinderProviders.RemoveAt(lastIndex);
+            options.Value.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
             options.Value.ModelBinderProviders.Add(new TestableComplexTypeModelBinderProvider());
 
             var factory = TestModelBinderFactory.Create(options.Value.ModelBinderProviders.ToArray());
@@ -1662,4 +1662,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
             }
         }
     }
+#pragma warning restore CS0618 // Type or member is obsolete
 }

+ 3 - 2
src/Mvc/Mvc.Core/test/ModelBinding/Binders/DictionaryModelBinderTest.cs

@@ -278,12 +278,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
 
             var binder = new DictionaryModelBinder<int, ModelWithProperties>(
                 new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
-                new ComplexTypeModelBinder(new Dictionary<ModelMetadata, IModelBinder>()
+                new ComplexObjectModelBinder(new Dictionary<ModelMetadata, IModelBinder>()
                 {
                     { valueMetadata.Properties["Id"], new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance) },
                     { valueMetadata.Properties["Name"], new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance) },
                 },
-                NullLoggerFactory.Instance),
+                Array.Empty<IModelBinder>(),
+                NullLogger<ComplexObjectModelBinder>.Instance),
                 NullLoggerFactory.Instance);
 
             // Act

+ 193 - 1
src/Mvc/Mvc.Core/test/ModelBinding/Metadata/DefaultBindingMetadataProviderTest.cs

@@ -658,6 +658,198 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             Assert.Equal(initialValue, context.BindingMetadata.IsBindingRequired);
         }
 
+        private class DefaultConstructorType { }
+
+        [Fact]
+        public void GetBoundConstructor_DefaultConstructor_ReturnsNull()
+        {
+            // Arrange
+            var type = typeof(DefaultConstructorType);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private class ParameterlessConstructorType
+        {
+            public ParameterlessConstructorType() { }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ParameterlessConstructor_ReturnsNull()
+        {
+            // Arrange
+            var type = typeof(ParameterlessConstructorType);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private class NonPublicParameterlessConstructorType
+        {
+            protected NonPublicParameterlessConstructorType() { }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_DoesNotReturnsNonPublicParameterlessConstructor()
+        {
+            // Arrange
+            var type = typeof(NonPublicParameterlessConstructorType);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private class MultipleConstructorType
+        {
+            public MultipleConstructorType() { }
+            public MultipleConstructorType(string prop) { }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ReturnsParameterlessConstructor_ForTypeWithMultipleConstructors()
+        {
+            // Arrange
+            var type = typeof(NonPublicParameterlessConstructorType);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private record RecordTypeWithPrimaryConstructor(string name)
+        {
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ReturnsPrimaryConstructor_ForRecordType()
+        {
+            // Arrange
+            var type = typeof(RecordTypeWithPrimaryConstructor);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.NotNull(result);
+            Assert.Collection(
+                result.GetParameters(),
+                p => Assert.Equal("name", p.Name));
+        }
+
+        private record RecordTypeWithDefaultConstructor
+        {
+            public string Name { get; init; }
+
+            public int Age { get; init; }
+        }
+
+        private record RecordTypeWithParameterlessConstructor
+        {
+            public RecordTypeWithParameterlessConstructor() { }
+
+            public string Name { get; init; }
+
+            public int Age { get; init; }
+        }
+
+        [Theory]
+        [InlineData(typeof(RecordTypeWithDefaultConstructor))]
+        [InlineData(typeof(RecordTypeWithParameterlessConstructor))]
+        public void GetBoundConstructor_ReturnsNull_ForRecordTypeWithParameterlessConstructor(Type type)
+        {
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private record RecordTypeWithMultipleConstructors(string Name)
+        {
+            public RecordTypeWithMultipleConstructors(string Name, int age) : this(Name) => Age = age;
+
+            public RecordTypeWithMultipleConstructors(int age) : this(string.Empty, age) { }
+
+            public int Age { get; set; }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ReturnsNull_ForRecordTypeWithMultipleConstructors()
+        {
+            // Arrange
+            var type = typeof(RecordTypeWithMultipleConstructors);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
+        private record RecordTypeWithConformingSynthesizedConstructor
+        {
+            public RecordTypeWithConformingSynthesizedConstructor(string Name, int Age)
+            {
+            }
+
+            public string Name { get; set; }
+
+            public int Age { get; set; }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ReturnsConformingSynthesizedConstructor()
+        {
+            // Arrange
+            var type = typeof(RecordTypeWithConformingSynthesizedConstructor);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.NotNull(result);
+            Assert.Collection(
+                result.GetParameters(),
+                p => Assert.Equal("Name", p.Name),
+                p => Assert.Equal("Age", p.Name));
+        }
+
+        private record RecordTypeWithNonConformingSynthesizedConstructor
+        {
+            public RecordTypeWithNonConformingSynthesizedConstructor(string name, string age)
+            {
+            }
+
+            public string Name { get; set; }
+
+            public int Age { get; set; }
+        }
+
+        [Fact]
+        public void GetBoundConstructor_ReturnsNull_IfSynthesizedConstructorIsNonConforming()
+        {
+            // Arrange
+            var type = typeof(RecordTypeWithNonConformingSynthesizedConstructor);
+
+            // Act
+            var result = DefaultBindingMetadataProvider.GetBoundConstructor(type);
+
+            // Assert
+            Assert.Null(result);
+        }
+
         [BindNever]
         private class BindNeverOnClass
         {
@@ -704,4 +896,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             public string Identifier { get; set; }
         }
     }
-}
+}

+ 313 - 44
src/Mvc/Mvc.Core/test/ModelBinding/Metadata/DefaultModelMetadataTest.cs

@@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
 
             var key = ModelMetadataIdentity.ForProperty(
-                typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), 
+                typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)),
                 typeof(string),
                 typeof(TypeWithProperties));
 
@@ -626,12 +626,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
 
             // Act
-            var firstPropertiesEvaluation = metadata.Properties;
+            var SinglePropertiesEvaluation = metadata.Properties;
             var secondPropertiesEvaluation = metadata.Properties;
 
             // Assert
             // Same IEnumerable<ModelMetadata> object.
-            Assert.Same(firstPropertiesEvaluation, secondPropertiesEvaluation);
+            Assert.Same(SinglePropertiesEvaluation, secondPropertiesEvaluation);
         }
 
         [Fact]
@@ -647,12 +647,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
 
             // Act
-            var firstPropertiesEvaluation = metadata.Properties.ToList();
+            var SinglePropertiesEvaluation = metadata.Properties.ToList();
             var secondPropertiesEvaluation = metadata.Properties.ToList();
 
             // Assert
             // Identical ModelMetadata objects every time we run through the Properties collection.
-            Assert.Equal(firstPropertiesEvaluation, secondPropertiesEvaluation);
+            Assert.Equal(SinglePropertiesEvaluation, secondPropertiesEvaluation);
         }
 
         [Fact]
@@ -924,7 +924,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
                 .GetMethod(nameof(CalculateHasValidators_ParameterMetadata_TypeHasNoValidatorsMethod), BindingFlags.Static | BindingFlags.NonPublic)
                 .GetParameters()[0];
             var modelIdentity = ModelMetadataIdentity.ForParameter(parameter);
-            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -942,7 +942,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var property = GetType()
                 .GetProperty(nameof(CalculateHasValidators_PropertyMetadata_TypeHasNoValidatorsProperty), BindingFlags.Static | BindingFlags.NonPublic);
             var modelIdentity = ModelMetadataIdentity.ForProperty(property, property.PropertyType, GetType());
-            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -958,7 +958,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         {
             // Arrange
             var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
-            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: false);
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -972,7 +972,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         {
             // Arrange
             var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
-            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), true);
+            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: true);
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -986,7 +986,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
         {
             // Arrange
             var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
-            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), null);
+            var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), hasValidators: null);
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -1002,7 +1002,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(TypeWithProperties);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var property = typeof(TypeWithProperties).GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty));
             var propertyIdentity = ModelMetadataIdentity.ForProperty(property, typeof(int), typeof(TypeWithProperties));
@@ -1027,13 +1027,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(TypeWithProperties);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var property1Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
-            var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
+            var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, hasValidators: false);
 
             var property2Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetProtectedSetProperty)), typeof(int), modelType);
-            var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, true);
+            var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, hasValidators: true);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1054,10 +1054,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(TypeWithProperties);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var propertyIdentity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
-            var propertyMetadata = CreateModelMetadata(propertyIdentity, metadataProvider.Object, null);
+            var propertyMetadata = CreateModelMetadata(propertyIdentity, metadataProvider.Object, hasValidators: null);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1078,13 +1078,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(TypeWithProperties);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var property1Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetPublicSetProperty)), typeof(int), modelType);
-            var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
+            var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, hasValidators: false);
 
             var property2Identity = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(TypeWithProperties.PublicGetProtectedSetProperty)), typeof(int), modelType);
-            var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, false);
+            var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, hasValidators: false);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1105,22 +1105,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(Employee);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
-            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
             var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
-            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
+            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
             var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
-            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
+            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
             var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
-            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
+            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
 
             var unitModel = typeof(BusinessUnit);
             var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
-            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
+            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: false);
             var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
-            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, true); // BusinessUnit.Id has validators.
+            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: true); // BusinessUnit.Id has validators.
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1146,22 +1146,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(Employee);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
-            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
             var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
-            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
+            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
             var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
-            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
+            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
             var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
-            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
+            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
 
             var unitModel = typeof(BusinessUnit);
             var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
-            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, true); // BusinessUnit.Head has validators
+            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: true); // BusinessUnit.Head has validators
             var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
-            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false); 
+            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: false);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1189,12 +1189,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(Employee);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
             var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
             var employeeEmployees = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
-            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
+            var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, hasValidators: false);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1202,7 +1202,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForType(modelType))
-                .Returns(CreateModelMetadata(modelIdentity, metadataProvider.Object, true)); // Employees.Employee has validators
+                .Returns(CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: true)); // Employees.Employee has validators
 
             // Act
             var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
@@ -1218,22 +1218,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             var modelType = typeof(Employee);
             var modelIdentity = ModelMetadataIdentity.ForType(modelType);
             var metadataProvider = new Mock<IModelMetadataProvider>();
-            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
 
             var employeeId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Id)), typeof(int), modelType);
-            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
+            var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
             var employeeUnit = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Unit)), typeof(BusinessUnit), modelType);
-            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
+            var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, hasValidators: false);
             var employeeManager = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Manager)), typeof(Employee), modelType);
-            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
+            var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, hasValidators: false);
             var employeeEmployeesId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(Employee.Employees)), typeof(List<Employee>), modelType);
-            var employeeEmployeesIdMetadata = CreateModelMetadata(employeeEmployeesId, metadataProvider.Object, false);
+            var employeeEmployeesIdMetadata = CreateModelMetadata(employeeEmployeesId, metadataProvider.Object, hasValidators: false);
 
             var unitModel = typeof(BusinessUnit);
             var unitHead = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Head)), typeof(Employee), unitModel);
-            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
+            var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, hasValidators: false);
             var unitId = ModelMetadataIdentity.ForProperty(unitModel.GetProperty(nameof(BusinessUnit.Id)), typeof(int), unitModel);
-            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false);
+            var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, hasValidators: false);
 
             metadataProvider
                 .Setup(mp => mp.GetMetadataForProperties(modelType))
@@ -1254,8 +1254,277 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
             Assert.False(result);
         }
 
+        private record SimpleRecordType(int Property);
+
+        [Fact]
+        public void CalculateHasValidators_RecordType_ParametersWithNoValidators()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordType);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordType.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
+
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            // Parameter has no validation metadata.
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
+
+            var constructorMetadata = CreateModelMetadata(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata)
+               .Verifiable();
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata })
+               .Verifiable();
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.False(result);
+            metadataProvider.Verify();
+        }
+
+        [Fact]
+        public void CalculateHasValidators_RecordType_ParametersWithValidators()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordType);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordType.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
+
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            // Parameter has some validation metadata.
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: true);
+
+            var constructorMetadata = CreateModelMetadata(ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata);
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata });
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.True(result);
+        }
+
+        private record SimpleRecordTypeWithProperty(int Property)
+        {
+            public int Property2 { get; set; }
+        }
+
+        [Fact]
+        public void CalculateHasValidators_RecordTypeWithProperty_NoValidators()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordTypeWithProperty);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
+
+            // Property2 has no validators
+            var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
+            var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
+
+            // Parameter named "Property" has no validators
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
+
+            var constructorMetadata = CreateModelMetadata(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata);
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata, property2Metadata });
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.False(result);
+        }
+
+        [Fact]
+        public void CalculateHasValidators_RecordTypeWithProperty_ParameteryHasValidators()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordTypeWithProperty);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
+
+            // Property2 has no validators
+            var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
+            var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
+
+            // Parameter named "Property" has validators
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: true);
+
+            var constructorMetadata = CreateModelMetadata(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata);
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata, property2Metadata });
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.True(result);
+        }
+
+        [Fact]
+        public void CalculateHasValidators_RecordTypeWithProperty_PropertyHasValidators()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordTypeWithProperty);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: false);
+
+            // Property2 has some validators
+            var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
+            var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: true);
+
+            // Parameter named "Property" has no validators
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
+
+            var constructorMetadata = CreateModelMetadata(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata);
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata, property2Metadata });
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.True(result);
+        }
+
+        [Fact]
+        public void CalculateHasValidators_RecordTypeWithProperty_MappedPropertyHasValidators_ValidatorsAreIgnored()
+        {
+            // Arrange
+            var modelType = typeof(SimpleRecordTypeWithProperty);
+            var constructor = modelType.GetConstructors().Single();
+            var parameter = constructor.GetParameters().Single();
+            var modelIdentity = ModelMetadataIdentity.ForType(modelType);
+            var metadataProvider = new Mock<ModelMetadataProvider>();
+            var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, hasValidators: false);
+            modelMetadata.BindingMetadata.BoundConstructor = constructor;
+
+            var propertyId = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property)), typeof(int), modelType);
+            var propertyMetadata = CreateModelMetadata(propertyId, metadataProvider.Object, hasValidators: true);
+
+            var property2Id = ModelMetadataIdentity.ForProperty(modelType.GetProperty(nameof(SimpleRecordTypeWithProperty.Property2)), typeof(int), modelType);
+            var property2Metadata = CreateModelMetadata(property2Id, metadataProvider.Object, hasValidators: false);
+
+            var parameterId = ModelMetadataIdentity.ForParameter(parameter);
+            var parameterMetadata = CreateModelMetadata(parameterId, metadataProvider.Object, hasValidators: false);
+
+            var constructorMetadata = CreateModelMetadata(
+                ModelMetadataIdentity.ForConstructor(constructor, modelType), metadataProvider.Object, hasValidators: null);
+            constructorMetadata.Details.BoundConstructorParameters = new[]
+            {
+                parameterMetadata,
+            };
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForConstructor(constructor, modelType))
+               .Returns(constructorMetadata);
+
+            metadataProvider
+               .Setup(mp => mp.GetMetadataForProperties(modelType))
+               .Returns(new[] { propertyMetadata, property2Metadata });
+
+            // Act
+            var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
+
+            // Assert
+            Assert.False(result);
+        }
+
         private static DefaultModelMetadata CreateModelMetadata(
-            ModelMetadataIdentity modelIdentity, 
+            ModelMetadataIdentity modelIdentity,
             IModelMetadataProvider metadataProvider,
             bool? hasValidators)
         {

+ 7 - 7
src/Mvc/Mvc.Core/test/ModelBinding/ModelBindingHelperTest.cs

@@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -490,7 +490,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(
@@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
             var binderProviders = new IModelBinderProvider[]
             {
                 new SimpleTypeModelBinderProvider(),
-                new ComplexTypeModelBinderProvider(),
+                new ComplexObjectModelBinderProvider(),
             };
 
             var validator = new DataAnnotationsModelValidatorProvider(

+ 4 - 4
src/Mvc/Mvc.DataAnnotations/test/ModelMetadataProviderTest.cs

@@ -630,7 +630,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
             var attributes = new[]
             {
                 new TestBinderTypeProvider(),
-                new TestBinderTypeProvider() { BinderType = typeof(ComplexTypeModelBinder) }
+                new TestBinderTypeProvider() { BinderType = typeof(ComplexObjectModelBinder) }
             };
 
             var provider = CreateProvider(attributes);
@@ -639,7 +639,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
             var metadata = provider.GetMetadataForType(typeof(string));
 
             // Assert
-            Assert.Same(typeof(ComplexTypeModelBinder), metadata.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), metadata.BinderType);
         }
 
         [Fact]
@@ -648,7 +648,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
             // Arrange
             var attributes = new[]
             {
-                new TestBinderTypeProvider() { BinderType = typeof(ComplexTypeModelBinder) },
+                new TestBinderTypeProvider() { BinderType = typeof(ComplexObjectModelBinder) },
                 new TestBinderTypeProvider() { BinderType = typeof(SimpleTypeModelBinder) }
             };
 
@@ -658,7 +658,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
             var metadata = provider.GetMetadataForType(typeof(string));
 
             // Assert
-            Assert.Same(typeof(ComplexTypeModelBinder), metadata.BinderType);
+            Assert.Same(typeof(ComplexObjectModelBinder), metadata.BinderType);
         }
 
         [Fact]

+ 1 - 1
src/Mvc/Mvc/test/MvcOptionsSetupTest.cs

@@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc
                 binder => Assert.IsType<DictionaryModelBinderProvider>(binder),
                 binder => Assert.IsType<ArrayModelBinderProvider>(binder),
                 binder => Assert.IsType<CollectionModelBinderProvider>(binder),
-                binder => Assert.IsType<ComplexTypeModelBinderProvider>(binder));
+                binder => Assert.IsType<ComplexObjectModelBinderProvider>(binder));
         }
 
         [Fact]

+ 34 - 0
src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs

@@ -299,6 +299,40 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
 #endif
         }
 
+        [Fact]
+        public async Task ValidationTagHelpers_UsingRecords()
+        {
+            // Arrange
+            var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Customer/HtmlGeneration_Customer/CustomerWithRecords");
+            var nameValueCollection = new List<KeyValuePair<string, string>>
+            {
+                new KeyValuePair<string,string>("Number", string.Empty),
+                new KeyValuePair<string,string>("Name", string.Empty),
+                new KeyValuePair<string,string>("Email", string.Empty),
+                new KeyValuePair<string,string>("PhoneNumber", string.Empty),
+                new KeyValuePair<string,string>("Password", string.Empty)
+            };
+            request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+            // Act
+            var response = await Client.SendAsync(request);
+
+            // Assert
+            var document = await response.GetHtmlDocumentAsync();
+
+            var validation = document.RequiredQuerySelector("span[data-valmsg-for=Number]");
+            Assert.Equal("The value '' is invalid.", validation.TextContent);
+
+            validation = document.QuerySelector("span[data-valmsg-for=Name]");
+            Assert.Null(validation);
+
+            validation = document.QuerySelector("span[data-valmsg-for=Email]");
+            Assert.Equal("field-validation-valid", validation.ClassName);
+
+            validation = document.QuerySelector("span[data-valmsg-for=Password]");
+            Assert.Equal("The Password field is required.", validation.TextContent);
+        }
+
         [Fact]
         public async Task CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents()
         {

+ 66 - 0
src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs

@@ -5,6 +5,7 @@ using System;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Text;
 using System.Text.Json.Serialization;
 using System.Threading.Tasks;
@@ -121,6 +122,71 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
             Assert.Equal(expected.StreetName, actual.StreetName);
         }
 
+        [Fact]
+        public virtual async Task JsonInputFormatter_RoundtripsRecordType()
+        {
+            // Arrange
+            var expected = new JsonFormatterController.SimpleRecordModel(18, "James", "JnK");
+
+            // Act
+            var response = await Client.PostAsJsonAsync("http://localhost/JsonFormatter/RoundtripRecordType/", expected);
+            var actual = await response.Content.ReadAsAsync<JsonFormatterController.SimpleRecordModel>();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal(expected.Id, actual.Id);
+            Assert.Equal(expected.Name, actual.Name);
+            Assert.Equal(expected.StreetName, actual.StreetName);
+        }
+
+        [Fact]
+        public virtual async Task JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors()
+        {
+            // Arrange
+            var expected = new JsonFormatterController.SimpleModelWithValidation(123, "This is a very long name", StreetName: null);
+
+            // Act
+            var response = await Client.PostAsJsonAsync($"JsonFormatter/{nameof(JsonFormatterController.RoundtripModelWithValidation)}", expected);
+
+            // Assert
+            await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+            var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
+            Assert.Collection(
+                problem.Errors.OrderBy(e => e.Key),
+                kvp =>
+                {
+                    Assert.Equal("Id", kvp.Key);
+                    Assert.Equal("The field Id must be between 1 and 100.", Assert.Single(kvp.Value));
+                },
+                kvp =>
+                {
+                    Assert.Equal("Name", kvp.Key);
+                    Assert.Equal("The field Name must be a string with a minimum length of 2 and a maximum length of 8.", Assert.Single(kvp.Value));
+                },
+                kvp =>
+                {
+                    Assert.Equal("StreetName", kvp.Key);
+                    Assert.Equal("The StreetName field is required.", Assert.Single(kvp.Value));
+                });
+        }
+
+        [Fact]
+        public virtual async Task JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors()
+        {
+            // Arrange
+            var expected = new JsonFormatterController.SimpleModelWithValidation(99, "TestName", "Some address");
+
+            // Act
+            var response = await Client.PostAsJsonAsync($"JsonFormatter/{nameof(JsonFormatterController.RoundtripModelWithValidation)}", expected);
+
+            // Assert
+            await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+            var actual = await response.Content.ReadFromJsonAsync<JsonFormatterController.SimpleModel>();
+            Assert.Equal(expected.Id, actual.Id);
+            Assert.Equal(expected.Name, actual.Name);
+            Assert.Equal(expected.StreetName, actual.StreetName);
+        }
+
         [Fact]
         public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType()
         {

+ 11 - 8
src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonInputFormatterTest.cs

@@ -13,13 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         {
         }
 
-        [Theory(Skip = "https://github.com/dotnet/corefx/issues/36025")]
-        [InlineData("\"I'm a JSON string!\"")]
-        [InlineData("true")]
-        [InlineData("\"\"")] // Empty string
-        public override Task JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(string input)
-        {
-            return base.JsonInputFormatter_ReturnsDefaultValue_ForValueTypes(input);
-        }
+        [Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
+        public override Task JsonInputFormatter_RoundtripsRecordType()
+            => base.JsonInputFormatter_RoundtripsRecordType();
+
+        [Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
+        public override Task JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors()
+            => base.JsonInputFormatter_ValidationWithRecordTypes_NoValidationErrors();
+
+        [Fact(Skip = "https://github.com/dotnet/runtime/issues/38539")]
+        public override Task JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors()
+            => base.JsonInputFormatter_ValidationWithRecordTypes_ValidationErrors();
     }
 }

+ 208 - 8
src/Mvc/test/Mvc.IntegrationTests/ActionParametersIntegrationTest.cs

@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
 using Microsoft.Extensions.Logging.Abstractions;
 using Xunit;
 
@@ -403,6 +404,72 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             });
             var modelState = testContext.ModelState;
 
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(
+                string.Format(
+                    "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. Record types must have a single primary constructor. " +
+                    "Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor.",
+                    typeof(ClassWithNoDefaultConstructor).FullName,
+                    nameof(Class1.Property1),
+                    typeof(Class1).FullName),
+                exception.Message);
+        }
+
+        public record ActionParameter_DefaultValueConstructor(string Name = "test", int Age = 23);
+
+        [Fact]
+        public async Task ActionParameter_UsesDefaultConstructorParameters()
+        {
+            // Arrange
+            var parameterType = typeof(ActionParameter_DefaultValueConstructor);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = parameterType
+            };
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Name", "James");
+            });
+            var modelState = testContext.ModelState;
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<ActionParameter_DefaultValueConstructor>(result.Model);
+            Assert.Equal("James", model.Name);
+            Assert.Equal(23, model.Age);
+        }
+
+        [Fact]
+        public async Task ActionParameter_UsingComplexTypeModelBinder_ModelPropertyTypeWithNoParameterlessConstructor_ThrowsException()
+        {
+            // Arrange
+            var parameterType = typeof(Class1);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = parameterType
+            };
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Name", "James").Add("Property1.City", "Seattle");
+            }, updateOptions: options =>
+            {
+                options.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
+#pragma warning disable CS0618 // Type or member is obsolete
+                options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
+#pragma warning restore CS0618 // Type or member is obsolete
+            });
+            var modelState = testContext.ModelState;
+
             // Act & Assert
             var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
             Assert.Equal(
@@ -434,21 +501,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             Assert.Equal(
                 string.Format(
                     "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.",
+                    "value types and must have a parameterless constructor. Record types must have a single primary constructor.",
                     typeof(PointStruct).FullName),
                 exception.Message);
         }
 
-        [Theory]
-        [InlineData(typeof(ClassWithNoDefaultConstructor))]
-        [InlineData(typeof(AbstractClassWithNoDefaultConstructor))]
-        public async Task ActionParameter_BindingToTypeWithNoParameterlessConstructor_ThrowsException(Type parameterType)
+        [Fact]
+        public async Task ActionParameter_BindingToAbstractionType_ThrowsException()
         {
             // Arrange
             var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
             var parameter = new ParameterDescriptor()
             {
-                ParameterType = parameterType,
+                ParameterType = typeof(AbstractClassWithNoDefaultConstructor),
                 Name = "p"
             };
             var testContext = ModelBindingTestHelper.GetTestContext();
@@ -458,8 +523,78 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             Assert.Equal(
                 string.Format(
                     "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.",
-                    parameterType.FullName),
+                    "value types and must have a parameterless constructor. Record types must have a single primary constructor.",
+                    typeof(AbstractClassWithNoDefaultConstructor).FullName),
+                exception.Message);
+        }
+
+        public class ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel
+        {
+            public ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel(string name = "default-name") => (Name) = (name);
+
+            public ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel(string name, int age) => (Name, Age) = (name, age);
+
+            public string Name { get; init; }
+
+            public int Age { get; init; }
+        }
+
+        [Fact]
+        public async Task ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructor_Throws()
+        {
+            // Arrange
+            var parameterType = typeof(ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = parameterType
+            };
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Name", "James");
+            });
+            var modelState = testContext.ModelState;
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(
+                string.Format(
+                        "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. Record types must have a single primary constructor.",
+                        typeof(ActionParameter_MultipleConstructorsWithDefaultValues_NoParameterlessConstructorModel).FullName),
+                    exception.Message);
+        }
+
+        public record ActionParameter_RecordTypeWithMultipleConstructors(string Name, int Age)
+        {
+            public ActionParameter_RecordTypeWithMultipleConstructors(string Name) : this(Name, 0) { }
+        }
+
+        [Fact]
+        public async Task ActionParameter_RecordTypeWithMultipleConstructors_Throws()
+        {
+            // Arrange
+            var parameterType = typeof(ActionParameter_RecordTypeWithMultipleConstructors);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = parameterType
+            };
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Name", "James").Add("Age", "29");
+            });
+            var modelState = testContext.ModelState;
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(
+                string.Format(
+                    "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. Record types must have a single primary constructor.",
+                    typeof(ActionParameter_RecordTypeWithMultipleConstructors).FullName),
                 exception.Message);
         }
 
@@ -527,6 +662,46 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             Assert.True(modelState.IsValid);
         }
 
+        [Fact]
+        public async Task ActionParameter_WithValidateNever_DoesNotGetValidated()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = ParameterWithValidateNever.ValidateNeverParameterInfo.Name,
+                ParameterType = typeof(ModelWithIValidatableObject)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create(nameof(ModelWithIValidatableObject.FirstName), "TestName");
+            });
+
+            var modelState = testContext.ModelState;
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider
+                .GetMetadataForParameter(ParameterWithValidateNever.ValidateNeverParameterInfo);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                parameter,
+                testContext,
+                modelMetadataProvider,
+                modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var model = Assert.IsType<ModelWithIValidatableObject>(modelBindingResult.Model);
+            Assert.Equal("TestName", model.FirstName);
+
+            // No validation errors are expected.
+            // Assert.True(modelState.IsValid);
+
+            // Tracking bug to enable this scenario: https://github.com/dotnet/aspnetcore/issues/24241
+            Assert.False(modelState.IsValid);
+        }
+
         [Theory]
         [InlineData(123, true)]
         [InlineData(null, false)]
@@ -800,6 +975,29 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             }
         }
 
+        private class ParameterWithValidateNever
+        {
+            public void MyAction([Required] string Name, [ValidateNever] ModelWithIValidatableObject validatableObject)
+            {
+            }
+
+            private static MethodInfo MyActionMethodInfo
+                => typeof(ParameterWithValidateNever).GetMethod(nameof(MyAction));
+
+            public static ParameterInfo NameParamterInfo
+                => MyActionMethodInfo.GetParameters()[0];
+
+            public static ParameterInfo ValidateNeverParameterInfo
+                => MyActionMethodInfo.GetParameters()[1];
+
+            public static ParameterInfo GetParameterInfo(string parameterName)
+            {
+                return MyActionMethodInfo
+                    .GetParameters()
+                    .Single(p => p.Name.Equals(parameterName, StringComparison.Ordinal));
+            }
+        }
+
         private class CustomReadOnlyCollection<T> : ICollection<T>
         {
             private ICollection<T> _original;
@@ -865,7 +1063,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
 
         // By default the ComplexTypeModelBinder fails to construct models for types with no parameterless constructor,
         // but a developer could change this behavior by overriding CreateModel
+#pragma warning disable CS0618 // Type or member is obsolete
         private class CustomComplexTypeModelBinder : ComplexTypeModelBinder
+#pragma warning restore CS0618 // Type or member is obsolete
         {
             public CustomComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
                 : base(propertyBinders, NullLoggerFactory.Instance)

+ 13 - 0
src/Mvc/test/Mvc.IntegrationTests/ComplexObjectIntegrationTest.cs

@@ -0,0 +1,13 @@
+// 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.
+
+using System;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+
+namespace Microsoft.AspNetCore.Mvc.IntegrationTests
+{
+    public class ComplexObjectIntegrationTest : ComplexTypeIntegrationTestBase
+    {
+        protected override Type ExpectedModelBinderType => typeof(ComplexObjectModelBinder);
+    }
+}

+ 4269 - 0
src/Mvc/test/Mvc.IntegrationTests/ComplexRecordIntegrationTest.cs

@@ -0,0 +1,4269 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.IntegrationTests
+{
+    // A clone of ComplexTypeIntegrationTestBase performed using record types
+    public class ComplexRecordIntegrationTest
+    {
+        private const string AddressBodyContent = "{ \"street\" : \"" + AddressStreetContent + "\" }";
+        private const string AddressStreetContent = "1 Microsoft Way";
+
+        private static readonly byte[] ByteArrayContent = Encoding.BigEndianUnicode.GetBytes("abcd");
+        private static readonly string ByteArrayEncoded = Convert.ToBase64String(ByteArrayContent);
+
+        private record Order1(int ProductId, Person1 Customer);
+
+        private record Person1(string Name, [FromBody] Address1 Address);
+
+        private record Address1(string Street);
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoBodyData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                request.ContentType = "application/json";
+            });
+
+            testContext.MvcOptions.AllowEmptyInputInBodyModelBinding = true;
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Address);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoBodyData_ValueInQuery()
+        {
+            // With record types, constructor parameters also appear as settable properties.
+            // In this case, we will only attempt to bind the parameter and not the property.
+
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill&paramater.Customer.Address=not-used");
+                request.ContentType = "application/json";
+            });
+
+            testContext.MvcOptions.AllowEmptyInputInBodyModelBinding = true;
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Address);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithPrefix_PartialData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=10");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
+
+            Assert.Equal(10, model.ProductId);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState).Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order3(int ProductId, Person3 Customer);
+
+        private record Person3(string Name, byte[] Token);
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Customer.Name=bill&parameter.Customer.Token=" + ByteArrayEncoded);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Equal(ByteArrayContent, model.Customer.Token);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Token").Value;
+            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
+            Assert.Equal(ByteArrayEncoded, entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithByteArrayModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill&Customer.Token=" + ByteArrayEncoded);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Equal(ByteArrayContent, model.Customer.Token);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Token").Value;
+            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
+            Assert.Equal(ByteArrayEncoded, entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Token);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        private record Order4(int ProductId, Person4 Customer);
+
+        private record Person4(string Name, IEnumerable<IFormFile> Documents);
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Single(model.Customer.Documents);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents").Value;
+            Assert.Null(entry.AttemptedValue); // FormFile entries for body don't include original text.
+            Assert.Null(entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithFormFileModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4),
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill");
+                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Single(model.Customer.Documents);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Documents").Value;
+            Assert.Null(entry.AttemptedValue); // FormFile entries don't include the model.
+            Assert.Null(entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoBodyData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+
+                // Deliberately leaving out any form data.
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Documents);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("parameter.Customer.Name", kvp.Key);
+            var entry = kvp.Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_PartialData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=10");
+                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            var document = Assert.Single(model.Customer.Documents);
+            Assert.Equal("text.txt", document.FileName);
+            using (var reader = new StreamReader(document.OpenReadStream()))
+            {
+                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Equal(10, model.ProductId);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents");
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            var document = Assert.Single(model.Customer.Documents);
+            Assert.Equal("text.txt", document.FileName);
+            using (var reader = new StreamReader(document.OpenReadStream()))
+            {
+                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Customer.Documents", entry.Key);
+        }
+
+        private record Order5(string Name, int[] ProductIds);
+
+        [Fact]
+        public async Task BindsArrayProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsArrayProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsArrayProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsArrayProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order6(string Name, List<int> ProductIds);
+
+        [Fact]
+        public async Task BindsListProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsListProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsListProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsListProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order7(string Name, Dictionary<string, int> ProductIds);
+
+        [Fact]
+        public async Task BindsDictionaryProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0].Key=key0&parameter.ProductIds[0].Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsDictionaryProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0].Key=key0&ProductIds[0].Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsDictionaryProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsDictionaryProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        // Dictionary property with an IEnumerable<> value type
+        private record Car1(string Name, Dictionary<string, IEnumerable<SpecDoc>> Specs);
+
+        // Dictionary property with an Array value type
+        private record Car2(string Name, Dictionary<string, SpecDoc[]> Specs);
+
+        private record Car3(string Name, IEnumerable<KeyValuePair<string, IEnumerable<SpecDoc>>> Specs);
+
+        private record SpecDoc(string Name);
+
+        [Fact]
+        public async Task BindsDictionaryProperty_WithIEnumerableComplexTypeValue_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car1>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsDictionaryProperty_WithArrayOfComplexTypeValue_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car2)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car2>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsDictionaryProperty_WithIEnumerableOfKeyValuePair_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car3)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car3>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        private record Order8(KeyValuePair<string, int> ProductId, string Name = default!);
+
+        [Fact]
+        public async Task BindsKeyValuePairProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductId.Key=key0&parameter.ProductId.Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task BindsKeyValuePairProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductId.Key=key0&ProductId.Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductId.Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        private record Car4(string Name, KeyValuePair<string, Dictionary<string, string>> Specs);
+
+        [Fact]
+        public async Task Foo_BindsKeyValuePairProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car4)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                                + "&p.Specs.Key=camera_specs"
+                                + "&p.Specs.Value[0].Key=spec1"
+                                + "&p.Specs.Value[0].Value=spec1.txt"
+                                + "&p.Specs.Value[1].Key=spec2"
+                                + "&p.Specs.Value[1].Value=spec2.txt";
+
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car4>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs.Value,
+                (e) =>
+                {
+                    Assert.Equal("spec1", e.Key);
+                    Assert.Equal("spec1.txt", e.Value);
+                },
+                (e) =>
+                {
+                    Assert.Equal("spec2", e.Key);
+                    Assert.Equal("spec2.txt", e.Value);
+                });
+
+            Assert.Equal(6, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Key").Value;
+            Assert.Equal("spec1", entry.AttemptedValue);
+            Assert.Equal("spec1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Value").Value;
+            Assert.Equal("spec1.txt", entry.AttemptedValue);
+            Assert.Equal("spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Key").Value;
+            Assert.Equal("spec2", entry.AttemptedValue);
+            Assert.Equal("spec2", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Value").Value;
+            Assert.Equal("spec2.txt", entry.AttemptedValue);
+            Assert.Equal("spec2.txt", entry.RawValue);
+        }
+
+        private record Order9(Person9 Customer);
+
+        private record Person9([FromBody] Address1 Address);
+
+        // If a nested POCO object has all properties bound from a greedy source, then it should be populated
+        // if the top-level object is created.
+        [Fact]
+        public async Task BindsNestedPOCO_WithAllGreedyBoundProperties()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order9)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order9>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order10([BindRequired] Person10 Customer);
+
+        private record Person10(string Name);
+
+        [Fact]
+        public async Task WithRequiredComplexProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order10)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext();
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order10>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer"].Errors);
+            Assert.Equal("A value for the 'Customer' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithBindRequired_NoData_AndCustomizedMessage_AddsGivenMessage()
+        {
+            // Arrange
+            var parameterInfo = typeof(Order10).GetConstructor(new[] { typeof(Person10) }).GetParameters()[0];
+            var metadataProvider = new TestModelMetadataProvider();
+            metadataProvider
+                .ForParameter(parameterInfo)
+                .BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
+                {
+                    // A real details provider could customize message based on BindingMetadataProviderContext.
+                    binding.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor(
+                        name => $"Hurts when '{ name }' is not provided.");
+                }));
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order10)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(metadataProvider: metadataProvider);
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order10>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer"].Errors);
+            Assert.Equal("Hurts when 'Customer' is not provided.", error.ErrorMessage);
+        }
+
+        private record Order11(Person11 Customer);
+
+        private record Person11(int Id, [BindRequired] string Name);
+
+        [Fact]
+        public async Task WithNestedRequiredProperty_WithPartialData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["parameter.Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithNestedRequiredProperty_WithData_EmptyPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithNestedRequiredProperty_WithData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?customParameter.Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        private record Order12([BindRequired] string ProductName);
+
+        [Fact]
+        public async Task WithRequiredProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Null(model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["ProductName"].Errors);
+            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithRequiredProperty_NoData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Null(model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.ProductName").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.ProductName"].Errors);
+            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithRequiredProperty_WithData_EmptyPrefix_GetsBound()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12),
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?ProductName=abc");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Equal("abc", model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
+            Assert.Equal("abc", entry.RawValue);
+            Assert.Equal("abc", entry.AttemptedValue);
+        }
+
+        private record Order13([BindRequired] List<int> OrderIds);
+
+        [Fact]
+        public async Task WithRequiredCollectionProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13)
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Null(model.OrderIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "OrderIds").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["OrderIds"].Errors);
+            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithRequiredCollectionProperty_NoData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Null(model.OrderIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.OrderIds").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.OrderIds"].Errors);
+            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task WithRequiredCollectionProperty_WithData_EmptyPrefix_GetsBound()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?OrderIds[0]=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Equal(new[] { 123 }, model.OrderIds.ToArray());
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "OrderIds[0]").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+        }
+
+        private record Order14(int ProductId);
+
+        // This covers the case where a key is present, but has an empty value. The type converter
+        // will report an error.
+        [Fact]
+        public async Task BindsPOCO_TypeConvertedPropertyNonConvertibleValue_GetsError()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order14)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order14>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(0, model.ProductId);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
+            Assert.Equal(string.Empty, entry.AttemptedValue);
+            Assert.Equal(string.Empty, entry.RawValue);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("The value '' is invalid.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        // This covers the case where a key is present, but has no value. The model binder will
+        // report and error because it's a value type (non-nullable).
+        [Fact]
+        [ReplaceCulture]
+        public async Task BindsPOCO_TypeConvertedPropertyWithEmptyValue_Error()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order14)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order14>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(0, model.ProductId);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("parameter.ProductId", entry.Key);
+            Assert.Equal(string.Empty, entry.Value.AttemptedValue);
+
+            var error = Assert.Single(entry.Value.Errors);
+            Assert.Equal("The value '' is invalid.", error.ErrorMessage, StringComparer.Ordinal);
+            Assert.Null(error.Exception);
+
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+        }
+
+        private record Person12(Address12 Address);
+
+        [ModelBinder(Name = "HomeAddress")]
+        private record Address12(string Street);
+
+        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with a class somewhere in the
+        // type hierarchy of an action parameter. This should behave identically to such an attribute on a property in
+        // the type hierarchy.
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task ModelNameOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Person12),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var person = Assert.IsType<Person12>(modelBindingResult.Model);
+            Assert.NotNull(person.Address);
+            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("HomeAddress.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with an action parameter's
+        // type. This should behave identically to such an attribute on an action parameter.
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task ModelNameOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Address12),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var address = Assert.IsType<Address12>(modelBindingResult.Model);
+            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("HomeAddress.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private record Person13(Address13 Address);
+
+        [Bind("Street")]
+        private record Address13(int Number, string Street, string City, string State);
+
+        // Make sure the metadata is honored when a [Bind] attribute is associated with a class somewhere in the type
+        // hierarchy of an action parameter. This should behave identically to such an attribute on a property in the
+        // type hierarchy. (Test is similar to ModelNameOnPropertyType_WithData_Succeeds() but covers implementing
+        // IPropertyFilterProvider, not IModelNameProvider.)
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task BindAttributeOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Person13),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString = new QueryString(
+                    "?Address.Number=23&Address.Street=someStreet&Address.City=Redmond&Address.State=WA"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var person = Assert.IsType<Person13>(modelBindingResult.Model);
+            Assert.NotNull(person.Address);
+            Assert.Null(person.Address.City);
+            Assert.Equal(0, person.Address.Number);
+            Assert.Null(person.Address.State);
+            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("Address.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        // Make sure the metadata is honored when a [Bind] attribute is associated with an action parameter's type.
+        // This should behave identically to such an attribute on an action parameter. (Test is similar
+        // to ModelNameOnParameterType_WithData_Succeeds() but covers implementing IPropertyFilterProvider, not
+        // IModelNameProvider.)
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task BindAttributeOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Address13),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString = new QueryString("?Number=23&Street=someStreet&City=Redmond&State=WA"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var address = Assert.IsType<Address13>(modelBindingResult.Model);
+            Assert.Null(address.City);
+            Assert.Equal(0, address.Number);
+            Assert.Null(address.State);
+            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private record Product(int ProductId)
+        {
+            public string Name { get; }
+
+            public IList<string> Aliases { get; }
+        }
+
+        [Theory]
+        [InlineData("?parameter.ProductId=10")]
+        [InlineData("?parameter.ProductId=10&parameter.Name=Camera")]
+        [InlineData("?parameter.ProductId=10&parameter.Name=Camera&parameter.Aliases[0]=Camera1")]
+        public async Task BindsSettableProperties(string queryString)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Product)
+            };
+
+            // Need to have a key here so that the ComplexTypeModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString(queryString);
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Product>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(10, model.ProductId);
+            Assert.Null(model.Name);
+            Assert.Null(model.Aliases);
+        }
+
+        private record Photo(string Id, KeyValuePair<string, LocationInfo> Info);
+
+        private record LocationInfo([FromHeader] string GpsCoordinates, int Zipcode);
+
+        [Fact]
+        public async Task BindsKeyValuePairProperty_HavingFromHeaderProperty_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Photo)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.Headers.Add("GpsCoordinates", "10,20");
+                request.QueryString = new QueryString("?Id=1&Info.Key=location1&Info.Value.Zipcode=98052");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            // Model
+            var model = Assert.IsType<Photo>(modelBindingResult.Model);
+            Assert.Equal("1", model.Id);
+            Assert.Equal("location1", model.Info.Key);
+            Assert.NotNull(model.Info.Value);
+            Assert.Equal("10,20", model.Info.Value.GpsCoordinates);
+            Assert.Equal(98052, model.Info.Value.Zipcode);
+
+            // ModelState
+            Assert.Equal(4, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Id").Value;
+            Assert.Equal("1", entry.AttemptedValue);
+            Assert.Equal("1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Key").Value;
+            Assert.Equal("location1", entry.AttemptedValue);
+            Assert.Equal("location1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Value.Zipcode").Value;
+            Assert.Equal("98052", entry.AttemptedValue);
+            Assert.Equal("98052", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Value.GpsCoordinates").Value;
+            Assert.Equal("10,20", entry.AttemptedValue);
+            Assert.Equal("10,20", entry.RawValue);
+        }
+
+        private record Person5(string Name, IFormFile Photo);
+
+        // Regression test for #4802.
+        [Fact]
+        public async Task ReportsFailureToCollectionModelBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(IList<Person5>),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "[0].Photo");
+
+                // CollectionModelBinder binds an empty collection when value providers are all empty.
+                request.QueryString = new QueryString("?a=b");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Person5>>(modelBindingResult.Model);
+            var person = Assert.Single(model);
+            Assert.Null(person.Name);
+            Assert.NotNull(person.Photo);
+            using (var reader = new StreamReader(person.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            var state = Assert.Single(modelState);
+            Assert.Equal("[0].Photo", state.Key);
+            Assert.Null(state.Value.AttemptedValue);
+            Assert.Empty(state.Value.Errors);
+            Assert.Null(state.Value.RawValue);
+        }
+
+        private record TestModel(TestInnerModel[] InnerModels);
+
+        private record TestInnerModel([ModelBinder(BinderType = typeof(NumberModelBinder))] decimal Rate);
+
+        private class NumberModelBinder : IModelBinder
+        {
+            private readonly NumberStyles _supportedStyles = NumberStyles.Float | NumberStyles.AllowThousands;
+            private DecimalModelBinder _innerBinder;
+
+            public NumberModelBinder(ILoggerFactory loggerFactory)
+            {
+                _innerBinder = new DecimalModelBinder(_supportedStyles, loggerFactory);
+            }
+
+            public Task BindModelAsync(ModelBindingContext bindingContext)
+            {
+                return _innerBinder.BindModelAsync(bindingContext);
+            }
+        }
+
+        // Regression test for #4939.
+        [Fact]
+        public async Task ReportsFailureToCollectionModelBinder_CustomBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(TestModel),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString(
+                    "?parameter.InnerModels[0].Rate=1,000.00&parameter.InnerModels[1].Rate=2000");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<TestModel>(modelBindingResult.Model);
+            Assert.NotNull(model.InnerModels);
+            Assert.Collection(
+                model.InnerModels,
+                item => Assert.Equal(1000, item.Rate),
+                item => Assert.Equal(2000, item.Rate));
+
+            Assert.True(modelState.IsValid);
+            Assert.Collection(
+                modelState,
+                kvp =>
+                {
+                    Assert.Equal("parameter.InnerModels[0].Rate", kvp.Key);
+                    Assert.Equal("1,000.00", kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Equal("1,000.00", kvp.Value.RawValue);
+                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
+                },
+                kvp =>
+                {
+                    Assert.Equal("parameter.InnerModels[1].Rate", kvp.Key);
+                    Assert.Equal("2000", kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Equal("2000", kvp.Value.RawValue);
+                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
+                });
+        }
+
+        private record Person6(string Name, Person6 Mother, IFormFile Photo);
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ReportsFailureToNearTopLevel()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person6),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person6>(modelBindingResult.Model);
+            Assert.Null(model.Mother);
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            var state = Assert.Single(modelState);
+            Assert.Equal("Photo", state.Key);
+            Assert.Null(state.Value.AttemptedValue);
+            Assert.Empty(state.Value.Errors);
+            Assert.Null(state.Value.RawValue);
+        }
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ReportsFailureToComplexTypeModelBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person6),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+                SetFormFileBodyContent(request, "Hello Mom!", "Mother.Photo");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person6>(modelBindingResult.Model);
+            Assert.NotNull(model.Mother);
+            Assert.Null(model.Mother.Mother);
+            Assert.NotNull(model.Mother.Photo);
+            using (var reader = new StreamReader(model.Mother.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello Mom!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            Assert.Collection(
+                modelState,
+                kvp =>
+                {
+                    Assert.Equal("Photo", kvp.Key);
+                    Assert.Null(kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Null(kvp.Value.RawValue);
+                },
+                kvp =>
+                {
+                    Assert.Equal("Mother.Photo", kvp.Key);
+                    Assert.Null(kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Null(kvp.Value.RawValue);
+                });
+        }
+
+        private record Person7(string Name, IList<Person7> Children, IFormFile Photo);
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ReportsFailureToViaCollection()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person7),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+                SetFormFileBodyContent(request, "Hello Fred!", "Children[0].Photo");
+                SetFormFileBodyContent(request, "Hello Ginger!", "Children[1].Photo");
+
+                request.QueryString = new QueryString("?Children[0].Name=Fred&Children[1].Name=Ginger");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person7>(modelBindingResult.Model);
+            Assert.NotNull(model.Children);
+            Assert.Collection(
+                model.Children,
+                item =>
+                {
+                    Assert.Null(item.Children);
+                    Assert.Equal("Fred", item.Name);
+                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
+                    {
+                        Assert.Equal("Hello Fred!", reader.ReadToEnd());
+                    }
+                },
+                item =>
+                {
+                    Assert.Null(item.Children);
+                    Assert.Equal("Ginger", item.Name);
+                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
+                    {
+                        Assert.Equal("Hello Ginger!", reader.ReadToEnd());
+                    }
+                });
+
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+        }
+
+        private record LoopyModel([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, LoopyModel SelfReference);
+
+        // Regression test for #7052
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn33Binders()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
+                $"potential nesting of '{typeof(LoopyModel)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(LoopyModel),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private record TwoDeepModel([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound);
+
+        private record ThreeDeepModel([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, TwoDeepModel Inner);
+
+        // Ensure model binding system allows MaxModelBindingRecursionDepth binders on the stack.
+        [Fact]
+        public async Task ModelBindingSystem_BindsWith3Binders()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(ThreeDeepModel),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
+
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelState.IsValid);
+            Assert.Equal(0, modelState.ErrorCount);
+
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ThreeDeepModel>(result.Model);
+            Assert.True(model.IsBound);
+            Assert.NotNull(model.Inner);
+            Assert.True(model.Inner.IsBound);
+        }
+
+        private record FourDeepModel([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, ThreeDeepModel Inner);
+
+        // Ensure model binding system disallows one more than MaxModelBindingRecursionDepth binders on the stack.
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn4Binders()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (3). Reduce the " +
+                $"potential nesting of '{typeof(FourDeepModel)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(FourDeepModel),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
+
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private record LoopyModel1([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, LoopyModel2 Inner);
+
+        private record LoopyModel2([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, LoopyModel3 Inner);
+
+        private record LoopyModel3([ModelBinder(typeof(SuccessfulModelBinder))] bool IsBound, LoopyModel1 Inner);
+
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn33Binders_WithIndirectModelTypeLoop()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
+                $"potential nesting of '{typeof(LoopyModel1)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(LoopyModel1),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private record RecordTypeWithSettableProperty1(string Name)
+        {
+            public int Age { get; set; }
+        }
+
+        [Fact]
+        public async Task RecordTypeWithBoundParametersAndProperties_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithSettableProperty1)
+            };
+
+            // Need to have a key here so that the ComplexObjectModelBinder will recurse to bind elements.
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithSettableProperty1>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Equal(0, model.Age);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        [Fact]
+        public async Task RecordTypeWithBoundParametersAndProperties_ValueForParameter()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithSettableProperty1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?name=TestName");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithSettableProperty1>(modelBindingResult.Model);
+            Assert.Equal("TestName", model.Name);
+            Assert.Equal(0, model.Age);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Name", entry.Key);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        [Fact]
+        public async Task RecordTypeWithBoundParametersAndProperties_ValueForProperty()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithSettableProperty1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?age=28");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithSettableProperty1>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Equal(28, model.Age);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Age", entry.Key);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        [Fact]
+        public async Task RecordTypeWithBoundParametersAndProperties_ValueForParameterAndProperty()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithSettableProperty1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=test&age=28");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithSettableProperty1>(modelBindingResult.Model);
+            Assert.Equal("test", model.Name);
+            Assert.Equal(28, model.Age);
+
+            Assert.Equal(2, modelState.Count);
+            var entry = Assert.Single(modelState, m => m.Key == "Age");
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            entry = Assert.Single(modelState, m => m.Key == "Name");
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        public record RecordTypeWithFilteredProperty1([BindNever] string Id, string Name);
+
+        [Fact]
+        public async Task RecordTypeWithBoundParameters_ParameterCannotBeBound()
+        {
+            // Annotatons on properties do not appear on properties. If an attribute is never bound, the property is also not bound.
+
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithFilteredProperty1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=not-bound&Name=test");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithFilteredProperty1>(modelBindingResult.Model);
+            Assert.Null(model.Id);
+            Assert.Equal("test", model.Name);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Name", entry.Key);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        [Bind(include: new[] { "Name" })]
+        public record RecordTypeWithFilteredProperty2(string Id, string Name);
+
+        [Fact]
+        public async Task RecordTypeWithBoundParameters_ParameterAreFiltered()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithFilteredProperty2)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=not-bound&Name=test");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithFilteredProperty2>(modelBindingResult.Model);
+            Assert.Null(model.Id);
+            Assert.Equal("test", model.Name);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Name", entry.Key);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        public record RecordTypesWithDifferentMetadataOnParameterAndProperty([FromQuery] string Id, string Name)
+        {
+            [FromHeader]
+            public string Id { get; init; } = Id;
+
+            public string Name { get; init; } = Name;
+        }
+
+        [Fact]
+        public async Task RecordTypesWithDifferentMetadataOnParameterAndProperty_MetadataOnParameterIsUsed()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDifferentMetadataOnParameterAndProperty)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.Headers.Add("Id", "not-bound");
+                request.QueryString = new QueryString("?Id=testId&Name=test");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypesWithDifferentMetadataOnParameterAndProperty>(modelBindingResult.Model);
+            Assert.Equal("testId", model.Id);
+            Assert.Equal("test", model.Name);
+
+            Assert.Single(modelState, e => e.Key == "Name");
+            Assert.Single(modelState, e => e.Key == "Id");
+        }
+
+        [Fact]
+        public async Task RecordTypesWithDifferentMetadataOnParameterAndProperty_NoDataForParameter()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDifferentMetadataOnParameterAndProperty)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.Headers.Add("Id", "not-bound");
+                request.QueryString = new QueryString("?Name=test");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypesWithDifferentMetadataOnParameterAndProperty>(modelBindingResult.Model);
+            Assert.Null(model.Id);
+            Assert.Equal("test", model.Name);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Name", entry.Key);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record RecordTypeWithCollectionParameter(string Id, IList<string> Tags);
+
+        [Fact]
+        public async Task RecordTypeWithCollectionParameter_WithData_Succeeds()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithCollectionParameter)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=test&Tags[0]=tag1&Tags[1]=tag2");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypeWithCollectionParameter>(modelBindingResult.Model);
+            Assert.Equal("test", model.Id);
+            Assert.Equal(new[] { "tag1", "tag2" }, model.Tags);
+
+            Assert.Single(modelState, e => e.Key == "Id");
+            Assert.Single(modelState, e => e.Key == "Tags[0]");
+            Assert.Single(modelState, e => e.Key == "Tags[1]");
+        }
+
+        [Fact]
+        public async Task RecordTypeCollectionParameter_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithCollectionParameter)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=test");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithCollectionParameter>(modelBindingResult.Model);
+            Assert.Equal("test", model.Id);
+            Assert.Null(model.Tags);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Id");
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record RecordTypesWithReadOnlyCollectionParameter(string Id, string[] Tags);
+
+        [Fact]
+        public async Task RecordTypesWithReadOnlyCollectionParameter_Data_GetsBound()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithReadOnlyCollectionParameter)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=test&Tags[0]=tag1&Tags[1]=tag2");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypesWithReadOnlyCollectionParameter>(modelBindingResult.Model);
+            Assert.Equal("test", model.Id);
+            Assert.Equal(new[] { "tag1", "tag2" }, model.Tags);
+
+            Assert.Single(modelState, e => e.Key == "Id");
+            Assert.Single(modelState, e => e.Key == "Tags[0]");
+            Assert.Single(modelState, e => e.Key == "Tags[1]");
+        }
+
+        private record RecordTypesWithDefaultParameterValue(string Id = "default-id", string[] Tags = null);
+
+        [Fact]
+        public async Task RecordTypesWithDefaultParameterValue_Data()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDefaultParameterValue)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=test&Tags[0]=tag1&Tags[1]=tag2");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+            Assert.Equal(0, modelState.ErrorCount);
+
+            var model = Assert.IsType<RecordTypesWithDefaultParameterValue>(modelBindingResult.Model);
+            Assert.Equal("test", model.Id);
+            Assert.Equal(new[] { "tag1", "tag2" }, model.Tags);
+
+            Assert.Single(modelState, e => e.Key == "Id");
+            Assert.Single(modelState, e => e.Key == "Tags[0]");
+            Assert.Single(modelState, e => e.Key == "Tags[1]");
+        }
+
+        [Fact]
+        public async Task RecordTypesWithDefaultParameterValue_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDefaultParameterValue)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypesWithDefaultParameterValue>(modelBindingResult.Model);
+            Assert.Equal("default-id", model.Id);
+            Assert.Null(model.Tags);
+
+            Assert.Empty(modelState);
+        }
+
+        [Fact]
+        public async Task RecordTypesWithDefaultParameterValue_PartialData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDefaultParameterValue)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Tags[0]=tag");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypesWithDefaultParameterValue>(modelBindingResult.Model);
+            Assert.Equal("default-id", model.Id);
+            Assert.Equal(new[] { "tag" }, model.Tags);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Tags[0]", entry.Key);
+        }
+
+        [Fact]
+        public async Task RecordTypesWithDefaultParameterValue_PartialDataWithPrefix()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypesWithDefaultParameterValue)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Tags[0]=tag");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypesWithDefaultParameterValue>(modelBindingResult.Model);
+            Assert.Equal("default-id", model.Id);
+            Assert.Equal(new[] { "tag" }, model.Tags);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            var entry = Assert.Single(modelState);
+            Assert.Equal("parameter.Tags[0]", entry.Key);
+        }
+
+        private record RecordTypeWithBindRequiredParameters([BindRequired] string Name, int Age);
+
+        [Fact]
+        public async Task RecordTypeWithBindRequiredParameters_Data_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithBindRequiredParameters)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=test&Age=7");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.True(modelState.IsValid);
+
+            var model = Assert.IsType<RecordTypeWithBindRequiredParameters>(modelBindingResult.Model);
+            Assert.Equal("test", model.Name);
+            Assert.Equal(7, model.Age);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.Equal(2, modelState.Count);
+            
+            Assert.Single(modelState, m => m.Key == "Age");
+            Assert.Single(modelState, m => m.Key == "Name");
+        }
+
+        [Fact]
+        public async Task RecordTypeWithBindRequiredParameters_PartialData_BindRequiredError()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecordTypeWithBindRequiredParameters)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Age=7");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecordTypeWithBindRequiredParameters>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Equal(7, model.Age);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(1, modelState.ErrorCount);
+            
+            Assert.Equal(2, modelState.Count);
+            var entry = Assert.Single(modelState, m => m.Key == "Age");
+            Assert.Empty(entry.Value.Errors);
+
+            entry = Assert.Single(modelState, m => m.Key == "Name");
+            var error = Assert.Single(entry.Value.Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        private static void SetJsonBodyContent(HttpRequest request, string content)
+        {
+            var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));
+            request.Body = stream;
+            request.ContentType = "application/json";
+        }
+
+        private static void SetFormFileBodyContent(HttpRequest request, string content, string name)
+        {
+            const string fileName = "text.txt";
+
+            FormFileCollection fileCollection;
+            if (request.HasFormContentType)
+            {
+                // Do less work and do not overwrite previous information if called a second time.
+                fileCollection = (FormFileCollection)request.Form.Files;
+            }
+            else
+            {
+                fileCollection = new FormFileCollection();
+                var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
+
+                request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
+                request.Form = formCollection;
+            }
+
+            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content));
+            var file = new FormFile(memoryStream, 0, memoryStream.Length, name, fileName)
+            {
+                Headers = new HeaderDictionary(),
+
+                // Do not move this up. Headers must be non-null before the ContentDisposition property is accessed.
+                ContentDisposition = $"form-data; name={name}; filename={fileName}",
+            };
+
+            fileCollection.Add(file);
+        }
+
+        private ModelMetadata GetMetadata(ModelBindingTestContext context, ParameterDescriptor parameter)
+        {
+            return context.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+        }
+
+        private IModelBinder GetModelBinder(
+            ModelBindingTestContext context,
+            ParameterDescriptor parameter,
+            ModelMetadata metadata)
+        {
+            var factory = ModelBindingTestHelper.GetModelBinderFactory(
+                context.MetadataProvider,
+                context.HttpContext.RequestServices);
+            var factoryContext = new ModelBinderFactoryContext
+            {
+                BindingInfo = parameter.BindingInfo,
+                CacheToken = parameter,
+                Metadata = metadata,
+            };
+
+            return factory.CreateBinder(factoryContext);
+        }
+    }
+}

+ 3774 - 0
src/Mvc/test/Mvc.IntegrationTests/ComplexTypeIntegrationTestBase.cs

@@ -0,0 +1,3774 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.IntegrationTests
+{
+    // Integration tests targeting the behavior of the ComplexTypeModelBinder and related classes
+    // with other model binders.
+    public abstract class ComplexTypeIntegrationTestBase
+    {
+        private const string AddressBodyContent = "{ \"street\" : \"" + AddressStreetContent + "\" }";
+        private const string AddressStreetContent = "1 Microsoft Way";
+
+        private static readonly byte[] ByteArrayContent = Encoding.BigEndianUnicode.GetBytes("abcd");
+        private static readonly string ByteArrayEncoded = Convert.ToBase64String(ByteArrayContent);
+
+        protected abstract Type ExpectedModelBinderType { get; }
+
+        [Fact]
+        public void ExpectedModelBinderIsConstructed()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+            var testContext = GetTestContext();
+
+            var metadata = GetMetadata(testContext, parameter);
+
+            // Act
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+
+            // Assert
+            Assert.Equal(ExpectedModelBinderType, modelBinder.GetType());
+        }
+
+        private class Order1
+        {
+            public int ProductId { get; set; }
+
+            public Person1 Customer { get; set; }
+        }
+
+        private class Person1
+        {
+            public string Name { get; set; }
+
+            [FromBody]
+            public Address1 Address { get; set; }
+        }
+
+        private class Address1
+        {
+            public string Street { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithBodyModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoBodyData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                request.ContentType = "application/json";
+            });
+
+            testContext.MvcOptions.AllowEmptyInputInBodyModelBinding = true;
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Address);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_PartialData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=10");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
+
+            Assert.Equal(10, model.ProductId);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState).Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private class Order3
+        {
+            public int ProductId { get; set; }
+
+            public Person3 Customer { get; set; }
+        }
+
+        private class Person3
+        {
+            public string Name { get; set; }
+
+            public byte[] Token { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Customer.Name=bill&parameter.Customer.Token=" + ByteArrayEncoded);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Equal(ByteArrayContent, model.Customer.Token);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Token").Value;
+            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
+            Assert.Equal(ByteArrayEncoded, entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill&Customer.Token=" + ByteArrayEncoded);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Equal(ByteArrayContent, model.Customer.Token);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Token").Value;
+            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
+            Assert.Equal(ByteArrayEncoded, entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Token);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        private class Order4
+        {
+            public int ProductId { get; set; }
+
+            public Person4 Customer { get; set; }
+        }
+
+        private class Person4
+        {
+            public string Name { get; set; }
+
+            public IEnumerable<IFormFile> Documents { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Single(model.Customer.Documents);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents").Value;
+            Assert.Null(entry.AttemptedValue); // FormFile entries for body don't include original text.
+            Assert.Null(entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithFormFileModelBinder_WithEmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Name=bill");
+                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Single(model.Customer.Documents);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Documents").Value;
+            Assert.Null(entry.AttemptedValue); // FormFile entries don't include the model.
+            Assert.Null(entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoBodyData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+
+                // Deliberately leaving out any form data.
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+            Assert.Null(model.Customer.Documents);
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("parameter.Customer.Name", kvp.Key);
+            var entry = kvp.Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_PartialData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=10");
+                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            var document = Assert.Single(model.Customer.Documents);
+            Assert.Equal("text.txt", document.FileName);
+            using (var reader = new StreamReader(document.OpenReadStream()))
+            {
+                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Equal(10, model.ProductId);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents");
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            var document = Assert.Single(model.Customer.Documents);
+            Assert.Equal("text.txt", document.FileName);
+            using (var reader = new StreamReader(document.OpenReadStream()))
+            {
+                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("Customer.Documents", entry.Key);
+        }
+
+        private class Order5
+        {
+            public string Name { get; set; }
+
+            public int[] ProductIds { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsArrayProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsArrayProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsArrayProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsArrayProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order5)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order5>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private class Order6
+        {
+            public string Name { get; set; }
+
+            public List<int> ProductIds { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsListProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsListProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
+            Assert.Equal("11", entry.AttemptedValue);
+            Assert.Equal("11", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsListProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsListProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private class Order7
+        {
+            public string Name { get; set; }
+
+            public Dictionary<string, int> ProductIds { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0].Key=key0&parameter.ProductIds[0].Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductIds[0].Key=key0&ProductIds[0].Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Null(model.ProductIds);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        // Dictionary property with an IEnumerable<> value type
+        private class Car1
+        {
+            public string Name { get; set; }
+
+            public Dictionary<string, IEnumerable<SpecDoc>> Specs { get; set; }
+        }
+
+        // Dictionary property with an Array value type
+        private class Car2
+        {
+            public string Name { get; set; }
+
+            public Dictionary<string, SpecDoc[]> Specs { get; set; }
+        }
+
+        private class Car3
+        {
+            public string Name { get; set; }
+
+            public IEnumerable<KeyValuePair<string, IEnumerable<SpecDoc>>> Specs { get; set; }
+        }
+
+        private class SpecDoc
+        {
+            public string Name { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_WithIEnumerableComplexTypeValue_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car1)
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car1>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_WithArrayOfComplexTypeValue_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car2)
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car2>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsDictionaryProperty_WithIEnumerableOfKeyValuePair_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car3)
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                        + "&p.Specs[0].Key=camera_specs"
+                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
+                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
+                        + "&p.Specs[1].Key=tyre_specs"
+                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
+                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car3>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs,
+                (e) =>
+                {
+                    Assert.Equal("camera_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("camera_spec2.txt", s.Name);
+                        });
+                },
+                (e) =>
+                {
+                    Assert.Equal("tyre_specs", e.Key);
+                    Assert.Collection(
+                        e.Value,
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec1.txt", s.Name);
+                        },
+                        (s) =>
+                        {
+                            Assert.Equal("tyre_spec2.txt", s.Name);
+                        });
+                });
+
+            Assert.Equal(7, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
+            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
+            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("camera_spec2.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
+            Assert.Equal("tyre_specs", entry.AttemptedValue);
+            Assert.Equal("tyre_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
+            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
+            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
+            Assert.Equal("tyre_spec2.txt", entry.RawValue);
+        }
+
+        private class Order8
+        {
+            public string Name { get; set; } = default!;
+
+            public KeyValuePair<string, int> ProductId { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsKeyValuePairProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString =
+                    new QueryString("?parameter.Name=bill&parameter.ProductId.Key=key0&parameter.ProductId.Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsKeyValuePairProperty_EmptyPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Name=bill&ProductId.Key=key0&ProductId.Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "ProductId.Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+        }
+
+        [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/11813")]
+        public async Task ComplexBinder_BindsKeyValuePairProperty_NoCollectionData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+            Assert.Equal(default, model.ProductId);
+
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
+            Assert.Single(entry.Errors);
+        }
+
+        [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/11813")]
+        public async Task ComplexBinder_BindsKeyValuePairProperty_NoData()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Null(model.Name);
+            Assert.Equal(default, model.ProductId);
+
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
+            Assert.Single(entry.Errors);
+        }
+
+        private class Car4
+        {
+            public string Name { get; set; }
+
+            public KeyValuePair<string, Dictionary<string, string>> Specs { get; set; }
+        }
+
+        [Fact]
+        public async Task Foo_ComplexBinder_BindsKeyValuePairProperty_WithPrefix_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "p",
+                ParameterType = typeof(Car4)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                var queryString = "?p.Name=Accord"
+                                + "&p.Specs.Key=camera_specs"
+                                + "&p.Specs.Value[0].Key=spec1"
+                                + "&p.Specs.Value[0].Value=spec1.txt"
+                                + "&p.Specs.Value[1].Key=spec2"
+                                + "&p.Specs.Value[1].Value=spec2.txt";
+
+                request.QueryString = new QueryString(queryString);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Car4>(modelBindingResult.Model);
+            Assert.Equal("Accord", model.Name);
+
+            Assert.Collection(
+                model.Specs.Value,
+                (e) =>
+                {
+                    Assert.Equal("spec1", e.Key);
+                    Assert.Equal("spec1.txt", e.Value);
+                },
+                (e) =>
+                {
+                    Assert.Equal("spec2", e.Key);
+                    Assert.Equal("spec2.txt", e.Value);
+                });
+
+            Assert.Equal(6, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+            Assert.Equal("Accord", entry.AttemptedValue);
+            Assert.Equal("Accord", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Key").Value;
+            Assert.Equal("camera_specs", entry.AttemptedValue);
+            Assert.Equal("camera_specs", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Key").Value;
+            Assert.Equal("spec1", entry.AttemptedValue);
+            Assert.Equal("spec1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Value").Value;
+            Assert.Equal("spec1.txt", entry.AttemptedValue);
+            Assert.Equal("spec1.txt", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Key").Value;
+            Assert.Equal("spec2", entry.AttemptedValue);
+            Assert.Equal("spec2", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Value").Value;
+            Assert.Equal("spec2.txt", entry.AttemptedValue);
+            Assert.Equal("spec2.txt", entry.RawValue);
+        }
+
+        private class Order9
+        {
+            public Person9 Customer { get; set; }
+        }
+
+        private class Person9
+        {
+            [FromBody]
+            public Address1 Address { get; set; }
+        }
+
+        // If a nested POCO object has all properties bound from a greedy source, then it should be populated
+        // if the top-level object is created.
+        [Fact]
+        public async Task ComplexBinder_BindsNestedPOCO_WithAllGreedyBoundProperties()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order9)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order9>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+
+            Assert.NotNull(model.Customer.Address);
+            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private class Order10
+        {
+            [BindRequired]
+            public Person10 Customer { get; set; }
+        }
+
+        private class Person10
+        {
+            public string Name { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredComplexProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order10)
+            };
+
+            // No Data
+            var testContext = GetTestContext();
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order10>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer"].Errors);
+            Assert.Equal("A value for the 'Customer' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithBindRequired_NoData_AndCustomizedMessage_AddsGivenMessage()
+        {
+            // Arrange
+            var metadataProvider = new TestModelMetadataProvider();
+            metadataProvider
+                .ForProperty(typeof(Order10), nameof(Order10.Customer))
+                .BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
+                {
+                    // A real details provider could customize message based on BindingMetadataProviderContext.
+                    binding.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor(
+                        name => $"Hurts when '{ name }' is not provided.");
+                }));
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order10)
+            };
+
+            // No Data
+            var testContext = GetTestContext(metadataProvider: metadataProvider);
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order10>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer"].Errors);
+            Assert.Equal("Hurts when 'Customer' is not provided.", error.ErrorMessage);
+        }
+
+        private class Order11
+        {
+            public Person11 Customer { get; set; }
+        }
+
+        private class Person11
+        {
+            public int Id { get; set; }
+
+            [BindRequired]
+            public string Name { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithNestedRequiredProperty_WithPartialData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11)
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["parameter.Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithNestedRequiredProperty_WithData_EmptyPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11)
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithNestedRequiredProperty_WithData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?customParameter.Customer.Id=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order11>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(123, model.Customer.Id);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Id").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors);
+            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        private class Order12
+        {
+            [BindRequired]
+            public string ProductName { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12)
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Null(model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["ProductName"].Errors);
+            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredProperty_NoData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Null(model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.ProductName").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.ProductName"].Errors);
+            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredProperty_WithData_EmptyPrefix_GetsBound()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12),
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?ProductName=abc");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Equal("abc", model.ProductName);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
+            Assert.Equal("abc", entry.RawValue);
+            Assert.Equal("abc", entry.AttemptedValue);
+        }
+
+        private class Order13
+        {
+            [BindRequired]
+            public List<int> OrderIds { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredCollectionProperty_NoData_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13)
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Null(model.OrderIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "OrderIds").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["OrderIds"].Errors);
+            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredCollectionProperty_NoData_CustomPrefix_GetsErrors()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13),
+                BindingInfo = new BindingInfo()
+                {
+                    BinderModelName = "customParameter"
+                }
+            };
+
+            // No Data
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Null(model.OrderIds);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "customParameter.OrderIds").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            var error = Assert.Single(modelState["customParameter.OrderIds"].Errors);
+            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task ComplexBinder_WithRequiredCollectionProperty_WithData_EmptyPrefix_GetsBound()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order13),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?OrderIds[0]=123");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order13>(modelBindingResult.Model);
+            Assert.Equal(new[] { 123 }, model.OrderIds.ToArray());
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "OrderIds[0]").Value;
+            Assert.Equal("123", entry.RawValue);
+            Assert.Equal("123", entry.AttemptedValue);
+        }
+
+        private class Order14
+        {
+            public int ProductId { get; set; }
+        }
+
+        // This covers the case where a key is present, but has an empty value. The type converter
+        // will report an error.
+        [Fact]
+        public async Task ComplexBinder_BindsPOCO_TypeConvertedPropertyNonConvertibleValue_GetsError()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order14)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId=");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order14>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(0, model.ProductId);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
+            Assert.Equal(string.Empty, entry.AttemptedValue);
+            Assert.Equal(string.Empty, entry.RawValue);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("The value '' is invalid.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        // This covers the case where a key is present, but has no value. The model binder will
+        // report and error because it's a value type (non-nullable).
+        [Fact]
+        [ReplaceCulture]
+        public async Task ComplexBinder_BindsPOCO_TypeConvertedPropertyWithEmptyValue_Error()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order14)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.ProductId");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order14>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(0, model.ProductId);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("parameter.ProductId", entry.Key);
+            Assert.Equal(string.Empty, entry.Value.AttemptedValue);
+
+            var error = Assert.Single(entry.Value.Errors);
+            Assert.Equal("The value '' is invalid.", error.ErrorMessage, StringComparer.Ordinal);
+            Assert.Null(error.Exception);
+
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+        }
+
+        private class Person12
+        {
+            public Address12 Address { get; set; }
+        }
+
+        [ModelBinder(Name = "HomeAddress")]
+        private class Address12
+        {
+            public string Street { get; set; }
+        }
+
+        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with a class somewhere in the
+        // type hierarchy of an action parameter. This should behave identically to such an attribute on a property in
+        // the type hierarchy.
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task ModelNameOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Person12),
+            };
+
+            var testContext = GetTestContext(
+                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var person = Assert.IsType<Person12>(modelBindingResult.Model);
+            Assert.NotNull(person.Address);
+            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("HomeAddress.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with an action parameter's
+        // type. This should behave identically to such an attribute on an action parameter.
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task ModelNameOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Address12),
+            };
+
+            var testContext = GetTestContext(
+                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var address = Assert.IsType<Address12>(modelBindingResult.Model);
+            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("HomeAddress.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private class Person13
+        {
+            public Address13 Address { get; set; }
+        }
+
+        [Bind("Street")]
+        private class Address13
+        {
+            public int Number { get; set; }
+
+            public string Street { get; set; }
+
+            public string City { get; set; }
+
+            public string State { get; set; }
+        }
+
+        // Make sure the metadata is honored when a [Bind] attribute is associated with a class somewhere in the type
+        // hierarchy of an action parameter. This should behave identically to such an attribute on a property in the
+        // type hierarchy. (Test is similar to ModelNameOnPropertyType_WithData_Succeeds() but covers implementing
+        // IPropertyFilterProvider, not IModelNameProvider.)
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task BindAttributeOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Person13),
+            };
+
+            var testContext = GetTestContext(
+                request => request.QueryString = new QueryString(
+                    "?Address.Number=23&Address.Street=someStreet&Address.City=Redmond&Address.State=WA"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var person = Assert.IsType<Person13>(modelBindingResult.Model);
+            Assert.NotNull(person.Address);
+            Assert.Null(person.Address.City);
+            Assert.Equal(0, person.Address.Number);
+            Assert.Null(person.Address.State);
+            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("Address.Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        // Make sure the metadata is honored when a [Bind] attribute is associated with an action parameter's type.
+        // This should behave identically to such an attribute on an action parameter. (Test is similar
+        // to ModelNameOnParameterType_WithData_Succeeds() but covers implementing IPropertyFilterProvider, not
+        // IModelNameProvider.)
+        [Theory]
+        [MemberData(
+            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
+            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
+        public async Task BindAttributeOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter-name",
+                BindingInfo = bindingInfo,
+                ParameterType = typeof(Address13),
+            };
+
+            var testContext = GetTestContext(
+                request => request.QueryString = new QueryString("?Number=23&Street=someStreet&City=Redmond&State=WA"));
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            var address = Assert.IsType<Address13>(modelBindingResult.Model);
+            Assert.Null(address.City);
+            Assert.Equal(0, address.Number);
+            Assert.Null(address.State);
+            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal("Street", kvp.Key);
+            var entry = kvp.Value;
+            Assert.NotNull(entry);
+            Assert.Empty(entry.Errors);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private class Product
+        {
+            public int ProductId { get; set; }
+
+            public string Name { get; }
+
+            public IList<string> Aliases { get; }
+        }
+
+        [Theory]
+        [InlineData("?parameter.ProductId=10")]
+        [InlineData("?parameter.ProductId=10&parameter.Name=Camera")]
+        [InlineData("?parameter.ProductId=10&parameter.Name=Camera&parameter.Aliases[0]=Camera1")]
+        public async Task ComplexTypeModelBinder_BindsSettableProperties(string queryString)
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Product)
+            };
+
+            // Need to have a key here so that the ComplexTypeModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString(queryString);
+                SetJsonBodyContent(request, AddressBodyContent);
+            });
+
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Product>(modelBindingResult.Model);
+            Assert.NotNull(model);
+            Assert.Equal(10, model.ProductId);
+            Assert.Null(model.Name);
+            Assert.Null(model.Aliases);
+        }
+
+        private class Photo
+        {
+            public string Id { get; set; }
+
+            public KeyValuePair<string, LocationInfo> Info { get; set; }
+        }
+
+        private class LocationInfo
+        {
+            [FromHeader]
+            public string GpsCoordinates { get; set; }
+
+            public int Zipcode { get; set; }
+        }
+
+        [Fact]
+        public async Task ComplexBinder_BindsKeyValuePairProperty_HavingFromHeaderProperty_Success()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Photo)
+            };
+
+            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
+            var testContext = GetTestContext(request =>
+            {
+                request.Headers.Add("GpsCoordinates", "10,20");
+                request.QueryString = new QueryString("?Id=1&Info.Key=location1&Info.Value.Zipcode=98052");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            // Model
+            var model = Assert.IsType<Photo>(modelBindingResult.Model);
+            Assert.Equal("1", model.Id);
+            Assert.Equal("location1", model.Info.Key);
+            Assert.NotNull(model.Info.Value);
+            Assert.Equal("10,20", model.Info.Value.GpsCoordinates);
+            Assert.Equal(98052, model.Info.Value.Zipcode);
+
+            // ModelState
+            Assert.Equal(4, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Id").Value;
+            Assert.Equal("1", entry.AttemptedValue);
+            Assert.Equal("1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Key").Value;
+            Assert.Equal("location1", entry.AttemptedValue);
+            Assert.Equal("location1", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Value.Zipcode").Value;
+            Assert.Equal("98052", entry.AttemptedValue);
+            Assert.Equal("98052", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "Info.Value.GpsCoordinates").Value;
+            Assert.Equal("10,20", entry.AttemptedValue);
+            Assert.Equal("10,20", entry.RawValue);
+        }
+
+        private class Person5
+        {
+            public string Name { get; set; }
+            public IFormFile Photo { get; set; }
+        }
+
+        // Regression test for #4802.
+        [Fact]
+        public async Task ComplexTypeModelBinder_ReportsFailureToCollectionModelBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(IList<Person5>),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "[0].Photo");
+
+                // CollectionModelBinder binds an empty collection when value providers are all empty.
+                request.QueryString = new QueryString("?a=b");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Person5>>(modelBindingResult.Model);
+            var person = Assert.Single(model);
+            Assert.Null(person.Name);
+            Assert.NotNull(person.Photo);
+            using (var reader = new StreamReader(person.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            var state = Assert.Single(modelState);
+            Assert.Equal("[0].Photo", state.Key);
+            Assert.Null(state.Value.AttemptedValue);
+            Assert.Empty(state.Value.Errors);
+            Assert.Null(state.Value.RawValue);
+        }
+
+        private class TestModel
+        {
+            public TestInnerModel[] InnerModels { get; set; } = Array.Empty<TestInnerModel>();
+        }
+
+        private class TestInnerModel
+        {
+            [ModelBinder(BinderType = typeof(NumberModelBinder))]
+            public decimal Rate { get; set; }
+        }
+
+        private class NumberModelBinder : IModelBinder
+        {
+            private readonly NumberStyles _supportedStyles = NumberStyles.Float | NumberStyles.AllowThousands;
+            private DecimalModelBinder _innerBinder;
+
+            public NumberModelBinder(ILoggerFactory loggerFactory)
+            {
+                _innerBinder = new DecimalModelBinder(_supportedStyles, loggerFactory);
+            }
+
+            public Task BindModelAsync(ModelBindingContext bindingContext)
+            {
+                return _innerBinder.BindModelAsync(bindingContext);
+            }
+        }
+
+        // Regression test for #4939.
+        [Fact]
+        public async Task ComplexTypeModelBinder_ReportsFailureToCollectionModelBinder_CustomBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(TestModel),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                request.QueryString = new QueryString(
+                    "?parameter.InnerModels[0].Rate=1,000.00&parameter.InnerModels[1].Rate=2000");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<TestModel>(modelBindingResult.Model);
+            Assert.NotNull(model.InnerModels);
+            Assert.Collection(
+                model.InnerModels,
+                item => Assert.Equal(1000, item.Rate),
+                item => Assert.Equal(2000, item.Rate));
+
+            Assert.True(modelState.IsValid);
+            Assert.Collection(
+                modelState,
+                kvp =>
+                {
+                    Assert.Equal("parameter.InnerModels[0].Rate", kvp.Key);
+                    Assert.Equal("1,000.00", kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Equal("1,000.00", kvp.Value.RawValue);
+                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
+                },
+                kvp =>
+                {
+                    Assert.Equal("parameter.InnerModels[1].Rate", kvp.Key);
+                    Assert.Equal("2000", kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Equal("2000", kvp.Value.RawValue);
+                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
+                });
+        }
+
+        private class Person6
+        {
+            public string Name { get; set; }
+
+            public Person6 Mother { get; set; }
+
+            public IFormFile Photo { get; set; }
+        }
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_NearTopLevel()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person6),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person6>(modelBindingResult.Model);
+            Assert.Null(model.Mother);
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            var state = Assert.Single(modelState);
+            Assert.Equal("Photo", state.Key);
+            Assert.Null(state.Value.AttemptedValue);
+            Assert.Empty(state.Value.Errors);
+            Assert.Null(state.Value.RawValue);
+        }
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person6),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+                SetFormFileBodyContent(request, "Hello Mom!", "Mother.Photo");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person6>(modelBindingResult.Model);
+            Assert.NotNull(model.Mother);
+            Assert.Null(model.Mother.Mother);
+            Assert.NotNull(model.Mother.Photo);
+            using (var reader = new StreamReader(model.Mother.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello Mom!", await reader.ReadToEndAsync());
+            }
+
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+            Assert.Collection(
+                modelState,
+                kvp =>
+                {
+                    Assert.Equal("Photo", kvp.Key);
+                    Assert.Null(kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Null(kvp.Value.RawValue);
+                },
+                kvp =>
+                {
+                    Assert.Equal("Mother.Photo", kvp.Key);
+                    Assert.Null(kvp.Value.AttemptedValue);
+                    Assert.Empty(kvp.Value.Errors);
+                    Assert.Null(kvp.Value.RawValue);
+                });
+        }
+
+        private class Person7
+        {
+            public string Name { get; set; }
+
+            public IList<Person7> Children { get; set; }
+
+            public IFormFile Photo { get; set; }
+        }
+
+        // Regression test for #6616.
+        [Fact]
+        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_ViaCollection()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Person7),
+            };
+
+            var testContext = GetTestContext(request =>
+            {
+                SetFormFileBodyContent(request, "Hello world!", "Photo");
+                SetFormFileBodyContent(request, "Hello Fred!", "Children[0].Photo");
+                SetFormFileBodyContent(request, "Hello Ginger!", "Children[1].Photo");
+
+                request.QueryString = new QueryString("?Children[0].Name=Fred&Children[1].Name=Ginger");
+            });
+
+            var modelState = testContext.ModelState;
+            var metadata = GetMetadata(testContext, parameter);
+            var modelBinder = GetModelBinder(testContext, parameter, metadata);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(
+                testContext,
+                modelBinder,
+                valueProvider,
+                parameter,
+                metadata,
+                value: null);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Person7>(modelBindingResult.Model);
+            Assert.NotNull(model.Children);
+            Assert.Collection(
+                model.Children,
+                item =>
+                {
+                    Assert.Null(item.Children);
+                    Assert.Equal("Fred", item.Name);
+                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
+                    {
+                        Assert.Equal("Hello Fred!", reader.ReadToEnd());
+                    }
+                },
+                item =>
+                {
+                    Assert.Null(item.Children);
+                    Assert.Equal("Ginger", item.Name);
+                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
+                    {
+                        Assert.Equal("Hello Ginger!", reader.ReadToEnd());
+                    }
+                });
+
+            Assert.Null(model.Name);
+            Assert.NotNull(model.Photo);
+            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
+            {
+                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
+            }
+
+            Assert.True(modelState.IsValid);
+        }
+
+        private class LoopyModel
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public LoopyModel SelfReference { get; set; }
+        }
+
+        // Regression test for #7052
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn33Binders()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
+                $"potential nesting of '{typeof(LoopyModel)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(LoopyModel),
+            };
+
+            var testContext = GetTestContext();
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private class TwoDeepModel
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+        }
+
+        private class ThreeDeepModel
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public TwoDeepModel Inner { get; set; }
+        }
+
+        // Ensure model binding system allows MaxModelBindingRecursionDepth binders on the stack.
+        [Fact]
+        public async Task ModelBindingSystem_BindsWith3Binders()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(ThreeDeepModel),
+            };
+
+            var testContext = GetTestContext(
+                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
+
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelState.IsValid);
+            Assert.Equal(0, modelState.ErrorCount);
+
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ThreeDeepModel>(result.Model);
+            Assert.True(model.IsBound);
+            Assert.NotNull(model.Inner);
+            Assert.True(model.Inner.IsBound);
+        }
+
+        private class FourDeepModel
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public ThreeDeepModel Inner { get; set; }
+        }
+
+        // Ensure model binding system disallows one more than MaxModelBindingRecursionDepth binders on the stack.
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn4Binders()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (3). Reduce the " +
+                $"potential nesting of '{typeof(FourDeepModel)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(FourDeepModel),
+            };
+
+            var testContext = GetTestContext(
+                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
+
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private class LoopyModel1
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public LoopyModel2 Inner { get; set; }
+        }
+
+        private class LoopyModel2
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public LoopyModel3 Inner { get; set; }
+        }
+
+        private class LoopyModel3
+        {
+            [ModelBinder(typeof(SuccessfulModelBinder))]
+            public bool IsBound { get; set; }
+
+            public LoopyModel1 Inner { get; set; }
+        }
+
+        [Fact]
+        public async Task ModelBindingSystem_ThrowsOn33Binders_WithIndirectModelTypeLoop()
+        {
+            // Arrange
+            var expectedMessage = $"Model binding system exceeded " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
+                $"potential nesting of '{typeof(LoopyModel1)}'. For example, this type may have a property with a " +
+                $"model binder that always succeeds. See the " +
+                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
+                $"information.";
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(LoopyModel1),
+            };
+
+            var testContext = GetTestContext();
+            var modelState = testContext.ModelState;
+            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                () => parameterBinder.BindModelAsync(parameter, testContext));
+            Assert.Equal(expectedMessage, exception.Message);
+        }
+
+        private static void SetJsonBodyContent(HttpRequest request, string content)
+        {
+            var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));
+            request.Body = stream;
+            request.ContentType = "application/json";
+        }
+
+        private static void SetFormFileBodyContent(HttpRequest request, string content, string name)
+        {
+            const string fileName = "text.txt";
+
+            FormFileCollection fileCollection;
+            if (request.HasFormContentType)
+            {
+                // Do less work and do not overwrite previous information if called a second time.
+                fileCollection = (FormFileCollection)request.Form.Files;
+            }
+            else
+            {
+                fileCollection = new FormFileCollection();
+                var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
+
+                request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
+                request.Form = formCollection;
+            }
+
+            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content));
+            var file = new FormFile(memoryStream, 0, memoryStream.Length, name, fileName)
+            {
+                Headers = new HeaderDictionary(),
+
+                // Do not move this up. Headers must be non-null before the ContentDisposition property is accessed.
+                ContentDisposition = $"form-data; name={name}; filename={fileName}",
+            };
+
+            fileCollection.Add(file);
+        }
+
+        private ModelMetadata GetMetadata(ModelBindingTestContext context, ParameterDescriptor parameter)
+        {
+            return context.MetadataProvider.GetMetadataForType(parameter.ParameterType);
+        }
+
+        private IModelBinder GetModelBinder(
+            ModelBindingTestContext context,
+            ParameterDescriptor parameter,
+            ModelMetadata metadata)
+        {
+            var factory = ModelBindingTestHelper.GetModelBinderFactory(
+                context.MetadataProvider,
+                context.HttpContext.RequestServices);
+            var factoryContext = new ModelBinderFactoryContext
+            {
+                BindingInfo = parameter.BindingInfo,
+                CacheToken = parameter,
+                Metadata = metadata,
+            };
+
+            return factory.CreateBinder(factoryContext);
+        }
+
+        protected virtual ModelBindingTestContext GetTestContext(
+            Action<HttpRequest> updateRequest = null,
+            Action<MvcOptions> updateOptions = null,
+            IModelMetadataProvider metadataProvider = null) 
+            => ModelBindingTestHelper.GetTestContext(updateRequest, updateOptions, actionDescriptor: null, metadataProvider);
+    }
+}

+ 16 - 3727
src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs

@@ -2,3745 +2,34 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
-using Microsoft.AspNetCore.Testing;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.IntegrationTests
 {
-    // Integration tests targeting the behavior of the ComplexTypeModelBinder and related classes
-    // with other model binders.
-    public class ComplexTypeModelBinderIntegrationTest
-    {
-        private const string AddressBodyContent = "{ \"street\" : \"" + AddressStreetContent + "\" }";
-        private const string AddressStreetContent = "1 Microsoft Way";
-
-        private static readonly byte[] ByteArrayContent = Encoding.BigEndianUnicode.GetBytes("abcd");
-        private static readonly string ByteArrayEncoded = Convert.ToBase64String(ByteArrayContent);
-
-        private class Order1
-        {
-            public int ProductId { get; set; }
-
-            public Person1 Customer { get; set; }
-        }
-
-        private class Person1
-        {
-            public string Name { get; set; }
-
-            [FromBody]
-            public Address1 Address { get; set; }
-        }
-
-        private class Address1
-        {
-            public string Street { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order1)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order1>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.NotNull(model.Customer.Address);
-            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithEmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order1)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Customer.Name=bill");
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order1>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.NotNull(model.Customer.Address);
-            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoBodyData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order1)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
-                request.ContentType = "application/json";
-            });
-
-            testContext.MvcOptions.AllowEmptyInputInBodyModelBinding = true;
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order1>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Null(model.Customer.Address);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_PartialData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order1)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.ProductId=10");
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order1>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
-
-            Assert.Equal(10, model.ProductId);
-
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState).Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order1)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order1>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
-
-            Assert.Empty(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-        }
-
-        private class Order3
-        {
-            public int ProductId { get; set; }
-
-            public Person3 Customer { get; set; }
-        }
-
-        private class Person3
-        {
-            public string Name { get; set; }
-
-            public byte[] Token { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order3)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString =
-                    new QueryString("?parameter.Customer.Name=bill&parameter.Customer.Token=" + ByteArrayEncoded);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order3>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Equal(ByteArrayContent, model.Customer.Token);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Token").Value;
-            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
-            Assert.Equal(ByteArrayEncoded, entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithEmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order3)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Customer.Name=bill&Customer.Token=" + ByteArrayEncoded);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order3>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Equal(ByteArrayContent, model.Customer.Token);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Customer.Token").Value;
-            Assert.Equal(ByteArrayEncoded, entry.AttemptedValue);
-            Assert.Equal(ByteArrayEncoded, entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithByteArrayModelBinder_WithPrefix_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order3)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order3>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Null(model.Customer.Token);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        private class Order4
-        {
-            public int ProductId { get; set; }
-
-            public Person4 Customer { get; set; }
-        }
-
-        private class Person4
-        {
-            public string Name { get; set; }
-
-            public IEnumerable<IFormFile> Documents { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
-                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order4>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Single(model.Customer.Documents);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents").Value;
-            Assert.Null(entry.AttemptedValue); // FormFile entries for body don't include original text.
-            Assert.Null(entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithEmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Customer.Name=bill");
-                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order4>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Single(model.Customer.Documents);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Customer.Documents").Value;
-            Assert.Null(entry.AttemptedValue); // FormFile entries don't include the model.
-            Assert.Null(entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoBodyData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
-
-                // Deliberately leaving out any form data.
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order4>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal("bill", model.Customer.Name);
-            Assert.Null(model.Customer.Documents);
-
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var kvp = Assert.Single(modelState);
-            Assert.Equal("parameter.Customer.Name", kvp.Key);
-            var entry = kvp.Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_PartialData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.ProductId=10");
-                SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order4>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-
-            var document = Assert.Single(model.Customer.Documents);
-            Assert.Equal("text.txt", document.FileName);
-            using (var reader = new StreamReader(document.OpenReadStream()))
-            {
-                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
-            }
-
-            Assert.Equal(10, model.ProductId);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents");
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-                SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order4>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-
-            var document = Assert.Single(model.Customer.Documents);
-            Assert.Equal("text.txt", document.FileName);
-            using (var reader = new StreamReader(document.OpenReadStream()))
-            {
-                Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
-            }
-
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState);
-            Assert.Equal("Customer.Documents", entry.Key);
-        }
-
-        private class Order5
-        {
-            public string Name { get; set; }
-
-            public int[] ProductIds { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsArrayProperty_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order5)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString =
-                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order5>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
-            Assert.Equal("11", entry.AttemptedValue);
-            Assert.Equal("11", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsArrayProperty_EmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order5)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order5>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new int[] { 10, 11 }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
-            Assert.Equal("11", entry.AttemptedValue);
-            Assert.Equal("11", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsArrayProperty_NoCollectionData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order5)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Name=bill");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order5>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsArrayProperty_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order5)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order5>(modelBindingResult.Model);
-            Assert.Null(model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Empty(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-        }
-
-        private class Order6
-        {
-            public string Name { get; set; }
-
-            public List<int> ProductIds { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsListProperty_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order6)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString =
-                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0]=10&parameter.ProductIds[1]=11");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order6>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0]").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[1]").Value;
-            Assert.Equal("11", entry.AttemptedValue);
-            Assert.Equal("11", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsListProperty_EmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order6)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Name=bill&ProductIds[0]=10&ProductIds[1]=11");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order6>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new List<int>() { 10, 11 }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0]").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[1]").Value;
-            Assert.Equal("11", entry.AttemptedValue);
-            Assert.Equal("11", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsListProperty_NoCollectionData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order6)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Name=bill");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order6>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsListProperty_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order6)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order6>(modelBindingResult.Model);
-            Assert.Null(model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Empty(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-        }
-
-        private class Order7
-        {
-            public string Name { get; set; }
-
-            public Dictionary<string, int> ProductIds { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order7)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString =
-                    new QueryString("?parameter.Name=bill&parameter.ProductIds[0].Key=key0&parameter.ProductIds[0].Value=10");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order7>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Key").Value;
-            Assert.Equal("key0", entry.AttemptedValue);
-            Assert.Equal("key0", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductIds[0].Value").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_EmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order7)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Name=bill&ProductIds[0].Key=key0&ProductIds[0].Value=10");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order7>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model.ProductIds);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Key").Value;
-            Assert.Equal("key0", entry.AttemptedValue);
-            Assert.Equal("key0", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductIds[0].Value").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_NoCollectionData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order7)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Name=bill");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order7>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order7)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order7>(modelBindingResult.Model);
-            Assert.Null(model.Name);
-            Assert.Null(model.ProductIds);
-
-            Assert.Empty(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-        }
-
-        // Dictionary property with an IEnumerable<> value type
-        private class Car1
-        {
-            public string Name { get; set; }
-
-            public Dictionary<string, IEnumerable<SpecDoc>> Specs { get; set; }
-        }
-
-        // Dictionary property with an Array value type
-        private class Car2
-        {
-            public string Name { get; set; }
-
-            public Dictionary<string, SpecDoc[]> Specs { get; set; }
-        }
-
-        private class Car3
-        {
-            public string Name { get; set; }
-
-            public IEnumerable<KeyValuePair<string, IEnumerable<SpecDoc>>> Specs { get; set; }
-        }
-
-        private class SpecDoc
-        {
-            public string Name { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithIEnumerableComplexTypeValue_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "p",
-                ParameterType = typeof(Car1)
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                var queryString = "?p.Name=Accord"
-                        + "&p.Specs[0].Key=camera_specs"
-                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
-                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
-                        + "&p.Specs[1].Key=tyre_specs"
-                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
-                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
-                request.QueryString = new QueryString(queryString);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Car1>(modelBindingResult.Model);
-            Assert.Equal("Accord", model.Name);
-
-            Assert.Collection(
-                model.Specs,
-                (e) =>
-                {
-                    Assert.Equal("camera_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec2.txt", s.Name);
-                        });
-                },
-                (e) =>
-                {
-                    Assert.Equal("tyre_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec2.txt", s.Name);
-                        });
-                });
-
-            Assert.Equal(7, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
-            Assert.Equal("Accord", entry.AttemptedValue);
-            Assert.Equal("Accord", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
-            Assert.Equal("camera_specs", entry.AttemptedValue);
-            Assert.Equal("camera_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
-            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
-            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec2.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
-            Assert.Equal("tyre_specs", entry.AttemptedValue);
-            Assert.Equal("tyre_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
-            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
-            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec2.txt", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithArrayOfComplexTypeValue_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "p",
-                ParameterType = typeof(Car2)
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                var queryString = "?p.Name=Accord"
-                        + "&p.Specs[0].Key=camera_specs"
-                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
-                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
-                        + "&p.Specs[1].Key=tyre_specs"
-                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
-                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
-                request.QueryString = new QueryString(queryString);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Car2>(modelBindingResult.Model);
-            Assert.Equal("Accord", model.Name);
-
-            Assert.Collection(
-                model.Specs,
-                (e) =>
-                {
-                    Assert.Equal("camera_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec2.txt", s.Name);
-                        });
-                },
-                (e) =>
-                {
-                    Assert.Equal("tyre_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec2.txt", s.Name);
-                        });
-                });
-
-            Assert.Equal(7, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
-            Assert.Equal("Accord", entry.AttemptedValue);
-            Assert.Equal("Accord", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
-            Assert.Equal("camera_specs", entry.AttemptedValue);
-            Assert.Equal("camera_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
-            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
-            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec2.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
-            Assert.Equal("tyre_specs", entry.AttemptedValue);
-            Assert.Equal("tyre_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
-            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
-            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec2.txt", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithIEnumerableOfKeyValuePair_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "p",
-                ParameterType = typeof(Car3)
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                var queryString = "?p.Name=Accord"
-                        + "&p.Specs[0].Key=camera_specs"
-                        + "&p.Specs[0].Value[0].Name=camera_spec1.txt"
-                        + "&p.Specs[0].Value[1].Name=camera_spec2.txt"
-                        + "&p.Specs[1].Key=tyre_specs"
-                        + "&p.Specs[1].Value[0].Name=tyre_spec1.txt"
-                        + "&p.Specs[1].Value[1].Name=tyre_spec2.txt";
-                request.QueryString = new QueryString(queryString);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Car3>(modelBindingResult.Model);
-            Assert.Equal("Accord", model.Name);
-
-            Assert.Collection(
-                model.Specs,
-                (e) =>
-                {
-                    Assert.Equal("camera_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("camera_spec2.txt", s.Name);
-                        });
-                },
-                (e) =>
-                {
-                    Assert.Equal("tyre_specs", e.Key);
-                    Assert.Collection(
-                        e.Value,
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec1.txt", s.Name);
-                        },
-                        (s) =>
-                        {
-                            Assert.Equal("tyre_spec2.txt", s.Name);
-                        });
-                });
-
-            Assert.Equal(7, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
-            Assert.Equal("Accord", entry.AttemptedValue);
-            Assert.Equal("Accord", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value;
-            Assert.Equal("camera_specs", entry.AttemptedValue);
-            Assert.Equal("camera_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value;
-            Assert.Equal("camera_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value;
-            Assert.Equal("camera_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("camera_spec2.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value;
-            Assert.Equal("tyre_specs", entry.AttemptedValue);
-            Assert.Equal("tyre_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value;
-            Assert.Equal("tyre_spec1.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value;
-            Assert.Equal("tyre_spec2.txt", entry.AttemptedValue);
-            Assert.Equal("tyre_spec2.txt", entry.RawValue);
-        }
-
-        private class Order8
-        {
-            public string Name { get; set; } = default!;
-
-            public KeyValuePair<string, int> ProductId { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order8)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString =
-                    new QueryString("?parameter.Name=bill&parameter.ProductId.Key=key0&parameter.ProductId.Value=10");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order8>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
-            Assert.Equal("key0", entry.AttemptedValue);
-            Assert.Equal("key0", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Value").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_EmptyPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order8)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Name=bill&ProductId.Key=key0&ProductId.Value=10");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order8>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(new KeyValuePair<string, int>("key0", 10), model.ProductId);
-
-            Assert.Equal(3, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
-            Assert.Equal("key0", entry.AttemptedValue);
-            Assert.Equal("key0", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "ProductId.Value").Value;
-            Assert.Equal("10", entry.AttemptedValue);
-            Assert.Equal("10", entry.RawValue);
-        }
-
-        [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/11813")]
-        public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_NoCollectionData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order8)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Name=bill");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order8>(modelBindingResult.Model);
-            Assert.Equal("bill", model.Name);
-            Assert.Equal(default, model.ProductId);
-
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
-            Assert.Equal("bill", entry.AttemptedValue);
-            Assert.Equal("bill", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
-            Assert.Single(entry.Errors);
-        }
-
-        [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/11813")]
-        public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_NoData()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order8)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order8>(modelBindingResult.Model);
-            Assert.Null(model.Name);
-            Assert.Equal(default, model.ProductId);
-
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
-            Assert.Single(entry.Errors);
-        }
-
-        private class Car4
-        {
-            public string Name { get; set; }
-
-            public KeyValuePair<string, Dictionary<string, string>> Specs { get; set; }
-        }
-
-        [Fact]
-        public async Task Foo_MutableObjectModelBinder_BindsKeyValuePairProperty_WithPrefix_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "p",
-                ParameterType = typeof(Car4)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                var queryString = "?p.Name=Accord"
-                                + "&p.Specs.Key=camera_specs"
-                                + "&p.Specs.Value[0].Key=spec1"
-                                + "&p.Specs.Value[0].Value=spec1.txt"
-                                + "&p.Specs.Value[1].Key=spec2"
-                                + "&p.Specs.Value[1].Value=spec2.txt";
-
-                request.QueryString = new QueryString(queryString);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Car4>(modelBindingResult.Model);
-            Assert.Equal("Accord", model.Name);
-
-            Assert.Collection(
-                model.Specs.Value,
-                (e) =>
-                {
-                    Assert.Equal("spec1", e.Key);
-                    Assert.Equal("spec1.txt", e.Value);
-                },
-                (e) =>
-                {
-                    Assert.Equal("spec2", e.Key);
-                    Assert.Equal("spec2.txt", e.Value);
-                });
-
-            Assert.Equal(6, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
-            Assert.Equal("Accord", entry.AttemptedValue);
-            Assert.Equal("Accord", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Key").Value;
-            Assert.Equal("camera_specs", entry.AttemptedValue);
-            Assert.Equal("camera_specs", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Key").Value;
-            Assert.Equal("spec1", entry.AttemptedValue);
-            Assert.Equal("spec1", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Value").Value;
-            Assert.Equal("spec1.txt", entry.AttemptedValue);
-            Assert.Equal("spec1.txt", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Key").Value;
-            Assert.Equal("spec2", entry.AttemptedValue);
-            Assert.Equal("spec2", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Value").Value;
-            Assert.Equal("spec2.txt", entry.AttemptedValue);
-            Assert.Equal("spec2.txt", entry.RawValue);
-        }
-
-        private class Order9
-        {
-            public Person9 Customer { get; set; }
-        }
-
-        private class Person9
-        {
-            [FromBody]
-            public Address1 Address { get; set; }
-        }
-
-        // If a nested POCO object has all properties bound from a greedy source, then it should be populated
-        // if the top-level object is created.
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsNestedPOCO_WithAllGreedyBoundProperties()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order9)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order9>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-
-            Assert.NotNull(model.Customer.Address);
-            Assert.Equal(AddressStreetContent, model.Customer.Address.Street);
-
-            Assert.Empty(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-        }
-
-        private class Order10
-        {
-            [BindRequired]
-            public Person10 Customer { get; set; }
-        }
-
-        private class Person10
-        {
-            public string Name { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredComplexProperty_NoData_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order10)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext();
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order10>(modelBindingResult.Model);
-            Assert.Null(model.Customer);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["Customer"].Errors);
-            Assert.Equal("A value for the 'Customer' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithBindRequired_NoData_AndCustomizedMessage_AddsGivenMessage()
-        {
-            // Arrange
-            var metadataProvider = new TestModelMetadataProvider();
-            metadataProvider
-                .ForProperty(typeof(Order10), nameof(Order10.Customer))
-                .BindingDetails((Action<ModelBinding.Metadata.BindingMetadata>)(binding =>
-                {
-                    // A real details provider could customize message based on BindingMetadataProviderContext.
-                    binding.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor(
-                        name => $"Hurts when '{ name }' is not provided.");
-                }));
-
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order10)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(metadataProvider: metadataProvider);
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order10>(modelBindingResult.Model);
-            Assert.Null(model.Customer);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["Customer"].Errors);
-            Assert.Equal("Hurts when 'Customer' is not provided.", error.ErrorMessage);
-        }
-
-        private class Order11
-        {
-            public Person11 Customer { get; set; }
-        }
-
-        private class Person11
-        {
-            public int Id { get; set; }
-
-            [BindRequired]
-            public string Name { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithNestedRequiredProperty_WithPartialData_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order11)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.Customer.Id=123");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order11>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal(123, model.Customer.Id);
-            Assert.Null(model.Customer.Name);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Id").Value;
-            Assert.Equal("123", entry.RawValue);
-            Assert.Equal("123", entry.AttemptedValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["parameter.Customer.Name"].Errors);
-            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithNestedRequiredProperty_WithData_EmptyPrefix_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order11)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?Customer.Id=123");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order11>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal(123, model.Customer.Id);
-            Assert.Null(model.Customer.Name);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Customer.Id").Value;
-            Assert.Equal("123", entry.RawValue);
-            Assert.Equal("123", entry.AttemptedValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Customer.Name").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["Customer.Name"].Errors);
-            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithNestedRequiredProperty_WithData_CustomPrefix_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order11),
-                BindingInfo = new BindingInfo()
-                {
-                    BinderModelName = "customParameter"
-                }
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?customParameter.Customer.Id=123");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order11>(modelBindingResult.Model);
-            Assert.NotNull(model.Customer);
-            Assert.Equal(123, model.Customer.Id);
-            Assert.Null(model.Customer.Name);
-
-            Assert.Equal(2, modelState.Count);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Id").Value;
-            Assert.Equal("123", entry.RawValue);
-            Assert.Equal("123", entry.AttemptedValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "customParameter.Customer.Name").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors);
-            Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        private class Order12
-        {
-            [BindRequired]
-            public string ProductName { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredProperty_NoData_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order12)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order12>(modelBindingResult.Model);
-            Assert.Null(model.ProductName);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["ProductName"].Errors);
-            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredProperty_NoData_CustomPrefix_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order12),
-                BindingInfo = new BindingInfo()
-                {
-                    BinderModelName = "customParameter"
-                }
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order12>(modelBindingResult.Model);
-            Assert.Null(model.ProductName);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "customParameter.ProductName").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["customParameter.ProductName"].Errors);
-            Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredProperty_WithData_EmptyPrefix_GetsBound()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order12),
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?ProductName=abc");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order12>(modelBindingResult.Model);
-            Assert.Equal("abc", model.ProductName);
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "ProductName").Value;
-            Assert.Equal("abc", entry.RawValue);
-            Assert.Equal("abc", entry.AttemptedValue);
-        }
-
-        private class Order13
-        {
-            [BindRequired]
-            public List<int> OrderIds { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredCollectionProperty_NoData_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order13)
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order13>(modelBindingResult.Model);
-            Assert.Null(model.OrderIds);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "OrderIds").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["OrderIds"].Errors);
-            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredCollectionProperty_NoData_CustomPrefix_GetsErrors()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order13),
-                BindingInfo = new BindingInfo()
-                {
-                    BinderModelName = "customParameter"
-                }
-            };
-
-            // No Data
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order13>(modelBindingResult.Model);
-            Assert.Null(model.OrderIds);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "customParameter.OrderIds").Value;
-            Assert.Null(entry.RawValue);
-            Assert.Null(entry.AttemptedValue);
-            var error = Assert.Single(modelState["customParameter.OrderIds"].Errors);
-            Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_WithRequiredCollectionProperty_WithData_EmptyPrefix_GetsBound()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order13),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?OrderIds[0]=123");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
 
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order13>(modelBindingResult.Model);
-            Assert.Equal(new[] { 123 }, model.OrderIds.ToArray());
-
-            Assert.Single(modelState);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "OrderIds[0]").Value;
-            Assert.Equal("123", entry.RawValue);
-            Assert.Equal("123", entry.AttemptedValue);
-        }
-
-        private class Order14
-        {
-            public int ProductId { get; set; }
-        }
-
-        // This covers the case where a key is present, but has an empty value. The type converter
-        // will report an error.
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyNonConvertibleValue_GetsError()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order14)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.ProductId=");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order14>(modelBindingResult.Model);
-            Assert.NotNull(model);
-            Assert.Equal(0, model.ProductId);
-
-            Assert.Single(modelState);
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
-            Assert.Equal(string.Empty, entry.AttemptedValue);
-            Assert.Equal(string.Empty, entry.RawValue);
-
-            var error = Assert.Single(entry.Errors);
-            Assert.Equal("The value '' is invalid.", error.ErrorMessage);
-            Assert.Null(error.Exception);
-        }
-
-        // This covers the case where a key is present, but has no value. The model binder will
-        // report and error because it's a value type (non-nullable).
-        [Fact]
-        [ReplaceCulture]
-        public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyWithEmptyValue_Error()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Order14)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString("?parameter.ProductId");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Order14>(modelBindingResult.Model);
-            Assert.NotNull(model);
-            Assert.Equal(0, model.ProductId);
-
-            var entry = Assert.Single(modelState);
-            Assert.Equal("parameter.ProductId", entry.Key);
-            Assert.Equal(string.Empty, entry.Value.AttemptedValue);
-
-            var error = Assert.Single(entry.Value.Errors);
-            Assert.Equal("The value '' is invalid.", error.ErrorMessage, StringComparer.Ordinal);
-            Assert.Null(error.Exception);
-
-            Assert.Equal(1, modelState.ErrorCount);
-            Assert.False(modelState.IsValid);
-        }
-
-        private class Person12
-        {
-            public Address12 Address { get; set; }
-        }
-
-        [ModelBinder(Name = "HomeAddress")]
-        private class Address12
-        {
-            public string Street { get; set; }
-        }
-
-        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with a class somewhere in the
-        // type hierarchy of an action parameter. This should behave identically to such an attribute on a property in
-        // the type hierarchy.
-        [Theory]
-        [MemberData(
-            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
-            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
-        public async Task ModelNameOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor
-            {
-                Name = "parameter-name",
-                BindingInfo = bindingInfo,
-                ParameterType = typeof(Person12),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-            var person = Assert.IsType<Person12>(modelBindingResult.Model);
-            Assert.NotNull(person.Address);
-            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
-
-            Assert.True(modelState.IsValid);
-            var kvp = Assert.Single(modelState);
-            Assert.Equal("HomeAddress.Street", kvp.Key);
-            var entry = kvp.Value;
-            Assert.NotNull(entry);
-            Assert.Empty(entry.Errors);
-            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
-        }
-
-        // Make sure the metadata is honored when a [ModelBinder] attribute is associated with an action parameter's
-        // type. This should behave identically to such an attribute on an action parameter.
-        [Theory]
-        [MemberData(
-            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
-            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
-        public async Task ModelNameOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor
-            {
-                Name = "parameter-name",
-                BindingInfo = bindingInfo,
-                ParameterType = typeof(Address12),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet"));
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-            var address = Assert.IsType<Address12>(modelBindingResult.Model);
-            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
-
-            Assert.True(modelState.IsValid);
-            var kvp = Assert.Single(modelState);
-            Assert.Equal("HomeAddress.Street", kvp.Key);
-            var entry = kvp.Value;
-            Assert.NotNull(entry);
-            Assert.Empty(entry.Errors);
-            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
-        }
-
-        private class Person13
-        {
-            public Address13 Address { get; set; }
-        }
-
-        [Bind("Street")]
-        private class Address13
-        {
-            public int Number { get; set; }
-
-            public string Street { get; set; }
-
-            public string City { get; set; }
-
-            public string State { get; set; }
-        }
-
-        // Make sure the metadata is honored when a [Bind] attribute is associated with a class somewhere in the type
-        // hierarchy of an action parameter. This should behave identically to such an attribute on a property in the
-        // type hierarchy. (Test is similar to ModelNameOnPropertyType_WithData_Succeeds() but covers implementing
-        // IPropertyFilterProvider, not IModelNameProvider.)
-        [Theory]
-        [MemberData(
-            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
-            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
-        public async Task BindAttributeOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo)
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor
-            {
-                Name = "parameter-name",
-                BindingInfo = bindingInfo,
-                ParameterType = typeof(Person13),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                request => request.QueryString = new QueryString(
-                    "?Address.Number=23&Address.Street=someStreet&Address.City=Redmond&Address.State=WA"));
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-            var person = Assert.IsType<Person13>(modelBindingResult.Model);
-            Assert.NotNull(person.Address);
-            Assert.Null(person.Address.City);
-            Assert.Equal(0, person.Address.Number);
-            Assert.Null(person.Address.State);
-            Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal);
-
-            Assert.True(modelState.IsValid);
-            var kvp = Assert.Single(modelState);
-            Assert.Equal("Address.Street", kvp.Key);
-            var entry = kvp.Value;
-            Assert.NotNull(entry);
-            Assert.Empty(entry.Errors);
-            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
-        }
-
-        // Make sure the metadata is honored when a [Bind] attribute is associated with an action parameter's type.
-        // This should behave identically to such an attribute on an action parameter. (Test is similar
-        // to ModelNameOnParameterType_WithData_Succeeds() but covers implementing IPropertyFilterProvider, not
-        // IModelNameProvider.)
-        [Theory]
-        [MemberData(
-            nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo),
-            MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))]
-        public async Task BindAttributeOnParameterType_WithData_Succeeds(BindingInfo bindingInfo)
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor
-            {
-                Name = "parameter-name",
-                BindingInfo = bindingInfo,
-                ParameterType = typeof(Address13),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                request => request.QueryString = new QueryString("?Number=23&Street=someStreet&City=Redmond&State=WA"));
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-            var address = Assert.IsType<Address13>(modelBindingResult.Model);
-            Assert.Null(address.City);
-            Assert.Equal(0, address.Number);
-            Assert.Null(address.State);
-            Assert.Equal("someStreet", address.Street, StringComparer.Ordinal);
-
-            Assert.True(modelState.IsValid);
-            var kvp = Assert.Single(modelState);
-            Assert.Equal("Street", kvp.Key);
-            var entry = kvp.Value;
-            Assert.NotNull(entry);
-            Assert.Empty(entry.Errors);
-            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
-        }
-
-        private class Product
-        {
-            public int ProductId { get; set; }
-
-            public string Name { get; }
-
-            public IList<string> Aliases { get; }
-        }
-
-        [Theory]
-        [InlineData("?parameter.ProductId=10")]
-        [InlineData("?parameter.ProductId=10&parameter.Name=Camera")]
-        [InlineData("?parameter.ProductId=10&parameter.Name=Camera&parameter.Aliases[0]=Camera1")]
-        public async Task ComplexTypeModelBinder_BindsSettableProperties(string queryString)
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Product)
-            };
-
-            // Need to have a key here so that the ComplexTypeModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString(queryString);
-                SetJsonBodyContent(request, AddressBodyContent);
-            });
-
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Product>(modelBindingResult.Model);
-            Assert.NotNull(model);
-            Assert.Equal(10, model.ProductId);
-            Assert.Null(model.Name);
-            Assert.Null(model.Aliases);
-        }
-
-        private class Photo
-        {
-            public string Id { get; set; }
-
-            public KeyValuePair<string, LocationInfo> Info { get; set; }
-        }
-
-        private class LocationInfo
-        {
-            [FromHeader]
-            public string GpsCoordinates { get; set; }
-
-            public int Zipcode { get; set; }
-        }
-
-        [Fact]
-        public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_HavingFromHeaderProperty_Success()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Photo)
-            };
-
-            // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.Headers.Add("GpsCoordinates", "10,20");
-                request.QueryString = new QueryString("?Id=1&Info.Key=location1&Info.Value.Zipcode=98052");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            // Model
-            var model = Assert.IsType<Photo>(modelBindingResult.Model);
-            Assert.Equal("1", model.Id);
-            Assert.Equal("location1", model.Info.Key);
-            Assert.NotNull(model.Info.Value);
-            Assert.Equal("10,20", model.Info.Value.GpsCoordinates);
-            Assert.Equal(98052, model.Info.Value.Zipcode);
-
-            // ModelState
-            Assert.Equal(4, modelState.Count);
-            Assert.Equal(0, modelState.ErrorCount);
-            Assert.True(modelState.IsValid);
-
-            var entry = Assert.Single(modelState, e => e.Key == "Id").Value;
-            Assert.Equal("1", entry.AttemptedValue);
-            Assert.Equal("1", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Info.Key").Value;
-            Assert.Equal("location1", entry.AttemptedValue);
-            Assert.Equal("location1", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Info.Value.Zipcode").Value;
-            Assert.Equal("98052", entry.AttemptedValue);
-            Assert.Equal("98052", entry.RawValue);
-
-            entry = Assert.Single(modelState, e => e.Key == "Info.Value.GpsCoordinates").Value;
-            Assert.Equal("10,20", entry.AttemptedValue);
-            Assert.Equal("10,20", entry.RawValue);
-        }
-
-        private class Person5
-        {
-            public string Name { get; set; }
-            public IFormFile Photo { get; set; }
-        }
-
-        // Regression test for #4802.
-        [Fact]
-        public async Task ComplexTypeModelBinder_ReportsFailureToCollectionModelBinder()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(IList<Person5>),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                SetFormFileBodyContent(request, "Hello world!", "[0].Photo");
-
-                // CollectionModelBinder binds an empty collection when value providers are all empty.
-                request.QueryString = new QueryString("?a=b");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<List<Person5>>(modelBindingResult.Model);
-            var person = Assert.Single(model);
-            Assert.Null(person.Name);
-            Assert.NotNull(person.Photo);
-            using (var reader = new StreamReader(person.Photo.OpenReadStream()))
-            {
-                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
-            }
-
-            Assert.True(modelState.IsValid);
-            var state = Assert.Single(modelState);
-            Assert.Equal("[0].Photo", state.Key);
-            Assert.Null(state.Value.AttemptedValue);
-            Assert.Empty(state.Value.Errors);
-            Assert.Null(state.Value.RawValue);
-        }
-
-        private class TestModel
-        {
-            public TestInnerModel[] InnerModels { get; set; } = Array.Empty<TestInnerModel>();
-        }
-
-        private class TestInnerModel
-        {
-            [ModelBinder(BinderType = typeof(NumberModelBinder))]
-            public decimal Rate { get; set; }
-        }
-
-        private class NumberModelBinder : IModelBinder
-        {
-            private readonly NumberStyles _supportedStyles = NumberStyles.Float | NumberStyles.AllowThousands;
-            private DecimalModelBinder _innerBinder;
-
-            public NumberModelBinder(ILoggerFactory loggerFactory)
-            {
-                _innerBinder = new DecimalModelBinder(_supportedStyles, loggerFactory);
-            }
-
-            public Task BindModelAsync(ModelBindingContext bindingContext)
-            {
-                return _innerBinder.BindModelAsync(bindingContext);
-            }
-        }
-
-        // Regression test for #4939.
-        [Fact]
-        public async Task ComplexTypeModelBinder_ReportsFailureToCollectionModelBinder_CustomBinder()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(TestModel),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                request.QueryString = new QueryString(
-                    "?parameter.InnerModels[0].Rate=1,000.00&parameter.InnerModels[1].Rate=2000");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<TestModel>(modelBindingResult.Model);
-            Assert.NotNull(model.InnerModels);
-            Assert.Collection(
-                model.InnerModels,
-                item => Assert.Equal(1000, item.Rate),
-                item => Assert.Equal(2000, item.Rate));
-
-            Assert.True(modelState.IsValid);
-            Assert.Collection(
-                modelState,
-                kvp =>
-                {
-                    Assert.Equal("parameter.InnerModels[0].Rate", kvp.Key);
-                    Assert.Equal("1,000.00", kvp.Value.AttemptedValue);
-                    Assert.Empty(kvp.Value.Errors);
-                    Assert.Equal("1,000.00", kvp.Value.RawValue);
-                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
-                },
-                kvp =>
-                {
-                    Assert.Equal("parameter.InnerModels[1].Rate", kvp.Key);
-                    Assert.Equal("2000", kvp.Value.AttemptedValue);
-                    Assert.Empty(kvp.Value.Errors);
-                    Assert.Equal("2000", kvp.Value.RawValue);
-                    Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
-                });
-        }
-
-        private class Person6
-        {
-            public string Name { get; set; }
-
-            public Person6 Mother { get; set; }
-
-            public IFormFile Photo { get; set; }
-        }
-
-        // Regression test for #6616.
-        [Fact]
-        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_NearTopLevel()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Person6),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                SetFormFileBodyContent(request, "Hello world!", "Photo");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Person6>(modelBindingResult.Model);
-            Assert.Null(model.Mother);
-            Assert.Null(model.Name);
-            Assert.NotNull(model.Photo);
-            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
-            {
-                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
-            }
-
-            Assert.True(modelState.IsValid);
-            var state = Assert.Single(modelState);
-            Assert.Equal("Photo", state.Key);
-            Assert.Null(state.Value.AttemptedValue);
-            Assert.Empty(state.Value.Errors);
-            Assert.Null(state.Value.RawValue);
-        }
+    public class ComplexTypeModelBinderIntegrationTest : ComplexTypeIntegrationTestBase
+    {
+#pragma warning disable CS0618 // Type or member is obsolete
+        protected override Type ExpectedModelBinderType => typeof(ComplexTypeModelBinder);
 
-        // Regression test for #6616.
-        [Fact]
-        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder()
+        protected override ModelBindingTestContext GetTestContext(
+            Action<HttpRequest> updateRequest = null,
+            Action<MvcOptions> updateOptions = null,
+            IModelMetadataProvider metadataProvider = null)
         {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Person6),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                SetFormFileBodyContent(request, "Hello world!", "Photo");
-                SetFormFileBodyContent(request, "Hello Mom!", "Mother.Photo");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Person6>(modelBindingResult.Model);
-            Assert.NotNull(model.Mother);
-            Assert.Null(model.Mother.Mother);
-            Assert.NotNull(model.Mother.Photo);
-            using (var reader = new StreamReader(model.Mother.Photo.OpenReadStream()))
-            {
-                Assert.Equal("Hello Mom!", await reader.ReadToEndAsync());
-            }
-
-            Assert.Null(model.Name);
-            Assert.NotNull(model.Photo);
-            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
-            {
-                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
-            }
-
-            Assert.True(modelState.IsValid);
-            Assert.Collection(
-                modelState,
-                kvp =>
+            return ModelBindingTestHelper.GetTestContext(
+                updateRequest,
+                updateOptions: options =>
                 {
-                    Assert.Equal("Photo", kvp.Key);
-                    Assert.Null(kvp.Value.AttemptedValue);
-                    Assert.Empty(kvp.Value.Errors);
-                    Assert.Null(kvp.Value.RawValue);
-                },
-                kvp =>
-                {
-                    Assert.Equal("Mother.Photo", kvp.Key);
-                    Assert.Null(kvp.Value.AttemptedValue);
-                    Assert.Empty(kvp.Value.Errors);
-                    Assert.Null(kvp.Value.RawValue);
-                });
-        }
-
-        private class Person7
-        {
-            public string Name { get; set; }
-
-            public IList<Person7> Children { get; set; }
-
-            public IFormFile Photo { get; set; }
-        }
+                    options.ModelBinderProviders.RemoveType<ComplexObjectModelBinderProvider>();
+                    options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
 
-        // Regression test for #6616.
-        [Fact]
-        public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_ViaCollection()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(Person7),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(request =>
-            {
-                SetFormFileBodyContent(request, "Hello world!", "Photo");
-                SetFormFileBodyContent(request, "Hello Fred!", "Children[0].Photo");
-                SetFormFileBodyContent(request, "Hello Ginger!", "Children[1].Photo");
-
-                request.QueryString = new QueryString("?Children[0].Name=Fred&Children[1].Name=Ginger");
-            });
-
-            var modelState = testContext.ModelState;
-            var metadata = GetMetadata(testContext, parameter);
-            var modelBinder = GetModelBinder(testContext, parameter, metadata);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var modelBindingResult = await parameterBinder.BindModelAsync(
-                testContext,
-                modelBinder,
-                valueProvider,
-                parameter,
-                metadata,
-                value: null);
-
-            // Assert
-            Assert.True(modelBindingResult.IsModelSet);
-
-            var model = Assert.IsType<Person7>(modelBindingResult.Model);
-            Assert.NotNull(model.Children);
-            Assert.Collection(
-                model.Children,
-                item =>
-                {
-                    Assert.Null(item.Children);
-                    Assert.Equal("Fred", item.Name);
-                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
-                    {
-                        Assert.Equal("Hello Fred!", reader.ReadToEnd());
-                    }
+                    updateOptions?.Invoke(options);
                 },
-                item =>
-                {
-                    Assert.Null(item.Children);
-                    Assert.Equal("Ginger", item.Name);
-                    using (var reader = new StreamReader(item.Photo.OpenReadStream()))
-                    {
-                        Assert.Equal("Hello Ginger!", reader.ReadToEnd());
-                    }
-                });
-
-            Assert.Null(model.Name);
-            Assert.NotNull(model.Photo);
-            using (var reader = new StreamReader(model.Photo.OpenReadStream()))
-            {
-                Assert.Equal("Hello world!", await reader.ReadToEndAsync());
-            }
-
-            Assert.True(modelState.IsValid);
-        }
-
-        private class LoopyModel
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public LoopyModel SelfReference { get; set; }
-        }
-
-        // Regression test for #7052
-        [Fact]
-        public async Task ModelBindingSystem_ThrowsOn33Binders()
-        {
-            // Arrange
-            var expectedMessage = $"Model binding system exceeded " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
-                $"potential nesting of '{typeof(LoopyModel)}'. For example, this type may have a property with a " +
-                $"model binder that always succeeds. See the " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
-                $"information.";
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(LoopyModel),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext();
-            var modelState = testContext.ModelState;
-            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act & Assert
-            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
-                () => parameterBinder.BindModelAsync(parameter, testContext));
-            Assert.Equal(expectedMessage, exception.Message);
-        }
-
-        private class TwoDeepModel
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-        }
-
-        private class ThreeDeepModel
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public TwoDeepModel Inner { get; set; }
-        }
-
-        // Ensure model binding system allows MaxModelBindingRecursionDepth binders on the stack.
-        [Fact]
-        public async Task ModelBindingSystem_BindsWith3Binders()
-        {
-            // Arrange
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(ThreeDeepModel),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
-
-            var modelState = testContext.ModelState;
-            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act
-            var result = await parameterBinder.BindModelAsync(parameter, testContext);
-
-            // Assert
-            Assert.True(modelState.IsValid);
-            Assert.Equal(0, modelState.ErrorCount);
-
-            Assert.True(result.IsModelSet);
-            var model = Assert.IsType<ThreeDeepModel>(result.Model);
-            Assert.True(model.IsBound);
-            Assert.NotNull(model.Inner);
-            Assert.True(model.Inner.IsBound);
-        }
-
-        private class FourDeepModel
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public ThreeDeepModel Inner { get; set; }
-        }
-
-        // Ensure model binding system disallows one more than MaxModelBindingRecursionDepth binders on the stack.
-        [Fact]
-        public async Task ModelBindingSystem_ThrowsOn4Binders()
-        {
-            // Arrange
-            var expectedMessage = $"Model binding system exceeded " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (3). Reduce the " +
-                $"potential nesting of '{typeof(FourDeepModel)}'. For example, this type may have a property with a " +
-                $"model binder that always succeeds. See the " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
-                $"information.";
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(FourDeepModel),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext(
-                updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
-
-            var modelState = testContext.ModelState;
-            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act & Assert
-            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
-                () => parameterBinder.BindModelAsync(parameter, testContext));
-            Assert.Equal(expectedMessage, exception.Message);
-        }
-
-        private class LoopyModel1
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public LoopyModel2 Inner { get; set; }
-        }
-
-        private class LoopyModel2
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public LoopyModel3 Inner { get; set; }
-        }
-
-        private class LoopyModel3
-        {
-            [ModelBinder(typeof(SuccessfulModelBinder))]
-            public bool IsBound { get; set; }
-
-            public LoopyModel1 Inner { get; set; }
-        }
-
-        [Fact]
-        public async Task ModelBindingSystem_ThrowsOn33Binders_WithIndirectModelTypeLoop()
-        {
-            // Arrange
-            var expectedMessage = $"Model binding system exceeded " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
-                $"potential nesting of '{typeof(LoopyModel1)}'. For example, this type may have a property with a " +
-                $"model binder that always succeeds. See the " +
-                $"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
-                $"information.";
-            var parameter = new ParameterDescriptor()
-            {
-                Name = "parameter",
-                ParameterType = typeof(LoopyModel1),
-            };
-
-            var testContext = ModelBindingTestHelper.GetTestContext();
-            var modelState = testContext.ModelState;
-            var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
-            var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
-            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
-
-            // Act & Assert
-            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
-                () => parameterBinder.BindModelAsync(parameter, testContext));
-            Assert.Equal(expectedMessage, exception.Message);
-        }
-
-        private static void SetJsonBodyContent(HttpRequest request, string content)
-        {
-            var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));
-            request.Body = stream;
-            request.ContentType = "application/json";
-        }
-
-        private static void SetFormFileBodyContent(HttpRequest request, string content, string name)
-        {
-            const string fileName = "text.txt";
-
-            FormFileCollection fileCollection;
-            if (request.HasFormContentType)
-            {
-                // Do less work and do not overwrite previous information if called a second time.
-                fileCollection = (FormFileCollection)request.Form.Files;
-            }
-            else
-            {
-                fileCollection = new FormFileCollection();
-                var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
-
-                request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
-                request.Form = formCollection;
-            }
-
-            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content));
-            var file = new FormFile(memoryStream, 0, memoryStream.Length, name, fileName)
-            {
-                Headers = new HeaderDictionary(),
-
-                // Do not move this up. Headers must be non-null before the ContentDisposition property is accessed.
-                ContentDisposition = $"form-data; name={name}; filename={fileName}",
-            };
-
-            fileCollection.Add(file);
-        }
-
-        private ModelMetadata GetMetadata(ModelBindingTestContext context, ParameterDescriptor parameter)
-        {
-            return context.MetadataProvider.GetMetadataForType(parameter.ParameterType);
-        }
-
-        private IModelBinder GetModelBinder(
-            ModelBindingTestContext context,
-            ParameterDescriptor parameter,
-            ModelMetadata metadata)
-        {
-            var factory = ModelBindingTestHelper.GetModelBinderFactory(
-                context.MetadataProvider,
-                context.HttpContext.RequestServices);
-            var factoryContext = new ModelBinderFactoryContext
-            {
-                BindingInfo = parameter.BindingInfo,
-                CacheToken = parameter,
-                Metadata = metadata,
-            };
-
-            return factory.CreateBinder(factoryContext);
+                metadataProvider: metadataProvider);
         }
+#pragma warning restore CS0618 // Type or member is obsolete
     }
 }

+ 1 - 0
src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj

@@ -3,6 +3,7 @@
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
     <UseSharedCompilation>false</UseSharedCompilation>
+    <LangVersion>9.0</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>

+ 174 - 1
src/Mvc/test/Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs

@@ -1139,6 +1139,179 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
             Assert.Equal(ModelValidationState.Valid, state.ValidationState);
         }
 
+        private record AddressRecord(string Street, string City)
+        {
+            public string ZipCode { get; set; }
+        }
+
+        [Fact]
+        public async Task TryUpdateModel_RecordTypeModel_DoesNotOverwriteConstructorParameters()
+        {
+            // Arrange
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Street", "SomeStreet");
+            });
+
+            var modelState = testContext.ModelState;
+            var model = new AddressRecord("DefaultStreet", "Toronto")
+            {
+                ZipCode = "98001",
+            };
+            var oldModel = model;
+
+            // Act
+            var result = await TryUpdateModelAsync(model, string.Empty, testContext);
+
+            // Assert
+            Assert.True(result);
+
+            // Model
+            Assert.Same(oldModel, model);
+            Assert.Equal("DefaultStreet", model.Street);
+            Assert.Equal("Toronto", model.City);
+            Assert.Equal("98001", model.ZipCode);
+
+            // ModelState
+            Assert.True(modelState.IsValid);
+            Assert.Empty(modelState);
+        }
+
+        [Fact]
+        public async Task TryUpdateModel_RecordTypeModel_UpdatesProperties()
+        {
+            // Arrange
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("ZipCode", "98007").Add("Street", "SomeStreet");
+            });
+
+            var modelState = testContext.ModelState;
+            var model = new AddressRecord("DefaultStreet", "Toronto")
+            {
+                ZipCode = "98001",
+            };
+            var oldModel = model;
+
+            // Act
+            var result = await TryUpdateModelAsync(model, string.Empty, testContext);
+
+            // Assert
+            Assert.True(result);
+
+            // Model
+            Assert.Same(oldModel, model);
+            Assert.Equal("DefaultStreet", model.Street);
+            Assert.Equal("Toronto", model.City);
+            Assert.Equal("98007", model.ZipCode);
+
+            // ModelState
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState);
+            Assert.Equal("ZipCode", entry.Key);
+            var state = entry.Value;
+            Assert.Equal("98007", state.AttemptedValue);
+            Assert.Equal("98007", state.RawValue);
+            Assert.Empty(state.Errors);
+            Assert.Equal(ModelValidationState.Valid, state.ValidationState);
+        }
+
+        private class ModelWithRecordTypeProperty
+        {
+            public AddressRecord Address { get; set; }
+        }
+
+        [Fact]
+        public async Task TryUpdateModel_RecordTypeProperty()
+        {
+            // Arrange
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Address.ZipCode", "98007").Add("Address.Street", "SomeStreet");
+            });
+
+            var modelState = testContext.ModelState;
+            var model = new ModelWithRecordTypeProperty();
+            var oldModel = model;
+
+            // Act
+            var result = await TryUpdateModelAsync(model, string.Empty, testContext);
+
+            // Assert
+            Assert.True(result);
+
+            // Model
+            Assert.Same(oldModel, model);
+            Assert.NotNull(model.Address);
+            var address = model.Address;
+            Assert.Equal("SomeStreet", address.Street);
+            Assert.Null(address.City);
+            Assert.Equal("98007", address.ZipCode);
+
+            // ModelState
+            Assert.True(modelState.IsValid);
+
+            Assert.Equal(2, modelState.Count);
+            var entry = Assert.Single(modelState, k => k.Key == "Address.ZipCode");
+            var state = entry.Value;
+            Assert.Equal("98007", state.AttemptedValue);
+            Assert.Equal("98007", state.RawValue);
+            Assert.Empty(state.Errors);
+            Assert.Equal(ModelValidationState.Valid, state.ValidationState);
+
+            entry = Assert.Single(modelState, k => k.Key == "Address.Street");
+            state = entry.Value;
+            Assert.Equal("SomeStreet", state.AttemptedValue);
+            Assert.Equal("SomeStreet", state.RawValue);
+            Assert.Empty(state.Errors);
+            Assert.Equal(ModelValidationState.Valid, state.ValidationState);
+        }
+
+        [Fact]
+        public async Task TryUpdateModel_RecordTypeProperty_InitializedDoesNotOverwriteConstructorParameters()
+        {
+            // Arrange
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = QueryString.Create("Address.ZipCode", "98007").Add("Address.Street", "SomeStreet");
+            });
+
+            var modelState = testContext.ModelState;
+            var model = new ModelWithRecordTypeProperty
+            {
+                Address = new AddressRecord("DefaultStreet", "DefaultCity")
+                {
+                    ZipCode = "98056",
+                },
+            };
+            var oldModel = model;
+
+            // Act
+            var result = await TryUpdateModelAsync(model, string.Empty, testContext);
+
+            // Assert
+            Assert.True(result);
+
+            // Model
+            Assert.Same(oldModel, model);
+            Assert.NotNull(model.Address);
+            var address = model.Address;
+            Assert.Equal("DefaultStreet", address.Street);
+            Assert.Equal("DefaultCity", address.City);
+            Assert.Equal("98007", address.ZipCode);
+
+            // ModelState
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState);
+            var state = entry.Value;
+            Assert.Equal("98007", state.AttemptedValue);
+            Assert.Equal("98007", state.RawValue);
+            Assert.Empty(state.Errors);
+            Assert.Equal(ModelValidationState.Valid, state.ValidationState);
+        }
+
         private void UpdateRequest(HttpRequest request, string data, string name)
         {
             const string fileName = "text.txt";
@@ -1237,4 +1410,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
                 ModelBindingTestHelper.GetObjectValidator(testContext.MetadataProvider));
         }
     }
-}
+}

+ 2307 - 0
src/Mvc/test/Mvc.IntegrationTests/ValidationWithRecordIntegrationTests.cs

@@ -0,0 +1,2307 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.IntegrationTests
+{
+    public class ValidationWithRecordIntegrationTests
+    {
+        private record TransferInfo([Range(25, 50)] int AccountId, double Amount);
+
+        private class TestController { }
+
+        public static TheoryData<List<ParameterDescriptor>> MultipleActionParametersAndValidationData
+        {
+            get
+            {
+                return new TheoryData<List<ParameterDescriptor>>
+                {
+                    // Irrespective of the order in which the parameters are defined on the action,
+                    // the validation on the TransferInfo's AccountId should occur.
+                    // Here 'accountId' parameter is bound by the prefix 'accountId' while the 'transferInfo'
+                    // property is bound using the empty prefix and the 'TransferInfo' property names.
+                    new List<ParameterDescriptor>()
+                    {
+                        new ParameterDescriptor()
+                        {
+                            Name = "accountId",
+                            ParameterType = typeof(int)
+                        },
+                        new ParameterDescriptor()
+                        {
+                            Name = "transferInfo",
+                            ParameterType = typeof(TransferInfo),
+                            BindingInfo = new BindingInfo()
+                            {
+                                BindingSource = BindingSource.Body
+                            }
+                        }
+                    },
+                    new List<ParameterDescriptor>()
+                    {
+                        new ParameterDescriptor()
+                        {
+                            Name = "transferInfo",
+                            ParameterType = typeof(TransferInfo),
+                            BindingInfo = new BindingInfo()
+                            {
+                                BindingSource = BindingSource.Body
+                            }
+                        },
+                        new ParameterDescriptor()
+                        {
+                            Name = "accountId",
+                            ParameterType = typeof(int)
+                        }
+                    }
+                };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(MultipleActionParametersAndValidationData))]
+        public async Task ValidationIsTriggered_OnFromBodyModels(List<ParameterDescriptor> parameters)
+        {
+            // Arrange
+            var actionDescriptor = new ControllerActionDescriptor()
+            {
+                BoundProperties = new List<ParameterDescriptor>(),
+                Parameters = parameters
+            };
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request =>
+                {
+                    request.QueryString = new QueryString("?accountId=30");
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"accountId\": 15,\"amount\": 250.0}"));
+                    request.ContentType = "application/json";
+                },
+                actionDescriptor: actionDescriptor);
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            foreach (var parameter in parameters)
+            {
+                await parameterBinder.BindModelAsync(parameter, testContext);
+            }
+
+            // Assert
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(
+                modelState,
+                e => string.Equals(e.Key, "AccountId", StringComparison.OrdinalIgnoreCase)).Value;
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal(ValidationAttributeUtil.GetRangeErrorMessage(25, 50, "AccountId"), error.ErrorMessage);
+        }
+
+        [Theory]
+        [MemberData(nameof(MultipleActionParametersAndValidationData))]
+        public async Task MultipleActionParameter_ValidModelState(List<ParameterDescriptor> parameters)
+        {
+            // Since validation attribute is only present on the FromBody model's property(TransferInfo's AccountId),
+            // validation should not trigger for the parameter which is bound from Uri.
+
+            // Arrange
+            var actionDescriptor = new ControllerActionDescriptor()
+            {
+                BoundProperties = new List<ParameterDescriptor>(),
+                Parameters = parameters
+            };
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request =>
+                {
+                    request.QueryString = new QueryString("?accountId=10");
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"accountId\": 40,\"amount\": 250.0}"));
+                    request.ContentType = "application/json";
+                },
+                actionDescriptor: actionDescriptor);
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            foreach (var parameter in parameters)
+            {
+                await parameterBinder.BindModelAsync(parameter, testContext);
+            }
+
+            // Assert
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order1([Required] string CustomerName);
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnSimpleTypeProperty_WithData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.CustomerName=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.Equal("bill", model.CustomerName);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.CustomerName").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnSimpleTypeProperty_NoData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order1)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order1>(modelBindingResult.Model);
+            Assert.Null(model.CustomerName);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "CustomerName").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            AssertRequiredError("CustomerName", error);
+        }
+
+        private record Order2([Required] Person2 Customer);
+
+
+        private record Person2(string Name);
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnPOCOProperty_WithData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order2)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order2>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnPOCOProperty_NoData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order2)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order2>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            AssertRequiredError("Customer", error);
+        }
+
+        private record Order3(Person3 Customer);
+
+        private record Person3(int Age, [Required] string Name);
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnNestedSimpleTypeProperty_WithData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal("bill", model.Customer.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnNestedSimpleTypeProperty_NoDataForRequiredProperty()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order3)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                // Force creation of the Customer model.
+                request.QueryString = new QueryString("?parameter.Customer.Age=17");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order3>(modelBindingResult.Model);
+            Assert.NotNull(model.Customer);
+            Assert.Equal(17, model.Customer.Age);
+            Assert.Null(model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            AssertRequiredError("Name", error);
+        }
+
+        private record Order4([Required] List<Item4> Items);
+
+        private record Item4(int ItemId);
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnCollectionProperty_WithData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Items[0].ItemId=17");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.NotNull(model.Items);
+            Assert.Equal(17, Assert.Single(model.Items).ItemId);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Items[0].ItemId").Value;
+            Assert.Equal("17", entry.AttemptedValue);
+            Assert.Equal("17", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnCollectionProperty_NoData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order4)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                // Force creation of the Customer model.
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order4>(modelBindingResult.Model);
+            Assert.Null(model.Items);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Items").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            AssertRequiredError("Items", error);
+        }
+
+        private record Order5([Required] int? ProductId, string Name);
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnPOCOPropertyOfBoundElement_WithData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(List<Order5>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter[0].ProductId=17");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Order5>>(modelBindingResult.Model);
+            Assert.Equal(17, Assert.Single(model).ProductId);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].ProductId").Value;
+            Assert.Equal("17", entry.AttemptedValue);
+            Assert.Equal("17", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_RequiredAttribute_OnPOCOPropertyOfBoundElement_NoDataForRequiredProperty()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(List<Order5>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                // Force creation of the Customer model.
+                request.QueryString = new QueryString("?parameter[0].Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Order5>>(modelBindingResult.Model);
+            var item = Assert.Single(model);
+            Assert.Null(item.ProductId);
+            Assert.Equal("bill", item.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].ProductId").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            AssertRequiredError("ProductId", error);
+        }
+
+        private record Order6([StringLength(5, ErrorMessage = "Too Long.")] string Name);
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfPOCO_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfPOCO_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order6)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Name=billybob");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order6>(modelBindingResult.Model);
+            Assert.Equal("billybob", model.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
+            Assert.Equal("billybob", entry.AttemptedValue);
+            Assert.Equal("billybob", entry.RawValue);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("Too Long.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        private record Order7(Person7 Customer);
+
+        private record Person7([StringLength(5, ErrorMessage = "Too Long.")] string Name);
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Customer.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=billybob");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Equal("billybob", model.Customer.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("billybob", entry.AttemptedValue);
+            Assert.Equal("billybob", entry.RawValue);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("Too Long.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfNestedPOCO_NoData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order7)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order7>(modelBindingResult.Model);
+            Assert.Null(model.Customer);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record Order8([ValidatePerson8] Person8 Customer);
+
+        private record Person8(string Name);
+
+        private class ValidatePerson8Attribute : ValidationAttribute
+        {
+            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+            {
+                if (((Person8)value).Name == "bill")
+                {
+                    return null;
+                }
+                else
+                {
+                    return new ValidationResult("Invalid Person.");
+                }
+            }
+        }
+
+        [Fact]
+        public async Task Validation_CustomAttribute_OnPOCOProperty_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("bill", model.Customer.Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_CustomAttribute_OnPOCOProperty_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order8)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Customer.Name=billybob");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order8>(modelBindingResult.Model);
+            Assert.Equal("billybob", model.Customer.Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
+            Assert.Equal("billybob", entry.AttemptedValue);
+            Assert.Equal("billybob", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Customer").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("Invalid Person.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        private record Order9([ValidateProducts9] List<Product9> Products);
+
+        private record Product9(string Name);
+
+        private class ValidateProducts9Attribute : ValidationAttribute
+        {
+            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+            {
+                if (((List<Product9>)value)[0].Name == "bill")
+                {
+                    return null;
+                }
+                else
+                {
+                    return new ValidationResult("Invalid Product.");
+                }
+            }
+        }
+
+        [Fact]
+        public async Task Validation_CustomAttribute_OnCollectionElement_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order9)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Products[0].Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order9>(modelBindingResult.Model);
+            Assert.Equal("bill", Assert.Single(model.Products).Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Products[0].Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_CustomAttribute_OnCollectionElement_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order9)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter.Products[0].Name=billybob");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order9>(modelBindingResult.Model);
+            Assert.Equal("billybob", Assert.Single(model.Products).Name);
+
+            Assert.Equal(2, modelState.Count);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter.Products[0].Name").Value;
+            Assert.Equal("billybob", entry.AttemptedValue);
+            Assert.Equal("billybob", entry.RawValue);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter.Products").Value;
+            Assert.Null(entry.RawValue);
+            Assert.Null(entry.AttemptedValue);
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("Invalid Product.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        private record Order10([StringLength(5, ErrorMessage = "Too Long.")] string Name);
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(List<Order10>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter[0].Name=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
+            Assert.Equal("bill", Assert.Single(model).Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Name").Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Empty(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(List<Order10>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter[0].Name=billybob");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
+            Assert.Equal("billybob", Assert.Single(model).Name);
+
+            Assert.Single(modelState);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Name").Value;
+            Assert.Equal("billybob", entry.AttemptedValue);
+            Assert.Equal("billybob", entry.RawValue);
+
+            var error = Assert.Single(entry.Errors);
+            Assert.Equal("Too Long.", error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        [Fact]
+        public async Task Validation_StringLengthAttribute_OnPropertyOfCollectionElement_NoData()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(List<Order10>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<Order10>>(modelBindingResult.Model);
+            Assert.Empty(model);
+
+            Assert.Empty(modelState);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+        }
+
+        private record User(int Id, uint Zip);
+
+        [Fact]
+        public async Task Validation_FormatException_ShowsInvalidValueMessage_OnSimpleTypeProperty()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(User)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Id=bill");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<User>(modelBindingResult.Model);
+            Assert.Equal(0, model.Id);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var state = Assert.Single(modelState);
+            Assert.Equal("Id", state.Key);
+            var entry = state.Value;
+            Assert.Equal("bill", entry.AttemptedValue);
+            Assert.Equal("bill", entry.RawValue);
+            Assert.Single(entry.Errors);
+
+            var error = entry.Errors[0];
+            Assert.Equal("The value 'bill' is not valid.", error.ErrorMessage);
+        }
+
+        [Fact]
+        public async Task Validation_OverflowException_ShowsInvalidValueMessage_OnSimpleTypeProperty()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(User)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Zip=-123");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<User>(modelBindingResult.Model);
+            Assert.Equal<uint>(0, model.Zip);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.False(modelState.IsValid);
+
+            var state = Assert.Single(modelState);
+            Assert.Equal("Zip", state.Key);
+            var entry = state.Value;
+            Assert.Equal("-123", entry.AttemptedValue);
+            Assert.Equal("-123", entry.RawValue);
+            Assert.Single(entry.Errors);
+
+            var error = entry.Errors[0];
+            Assert.Equal("The value '-123' is not valid.", error.ErrorMessage);
+        }
+
+        private record NeverValid(string NeverValidProperty)  : IValidatableObject
+        {
+            public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+            {
+                var result = new ValidationResult(
+                    $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
+                    $"to its {nameof(NeverValid)} type.");
+                return new[] { result };
+            }
+        }
+
+        private class NeverValidAttribute : ValidationAttribute
+        {
+            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+            {
+                // By default, ValidationVisitor visits _all_ properties within a non-null complex object.
+                // But, like most reasonable ValidationAttributes, NeverValidAttribute ignores null property values.
+                if (value == null)
+                {
+                    return ValidationResult.Success;
+                }
+
+                return new ValidationResult(
+                    $"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
+                    $"to its associated {nameof(NeverValidAttribute)}.");
+            }
+        }
+
+        private record ValidateSomeProperties(
+            [Display(Name = "Not ever valid")] NeverValid NeverValidBecauseType,
+
+            [NeverValid]
+            [Display(Name = "Never valid")]
+            string NeverValidBecauseAttribute,
+
+            [ValidateNever]
+            [NeverValid]
+            string ValidateNever)
+        {
+
+            [ValidateNever]
+            public int ValidateNeverLength => ValidateNever.Length;
+        }
+
+        [Fact]
+        public async Task IValidatableObject_IsValidated()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomeProperties),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString
+                    = new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}=1"));
+
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomeProperties>(result.Model);
+            Assert.Equal("1", model.NeverValidBecauseType.NeverValidProperty);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(1, modelState.ErrorCount);
+            Assert.Collection(
+                modelState,
+                state =>
+                {
+                    Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseType), state.Key);
+                    Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState);
+
+                    var error = Assert.Single(state.Value.Errors);
+                    Assert.Equal(
+                        "'NeverValidBecauseType' (display: 'Not ever valid') is not valid due to its NeverValid type.",
+                        error.ErrorMessage);
+                    Assert.Null(error.Exception);
+                },
+                state =>
+                {
+                    Assert.Equal(
+                        $"{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}",
+                        state.Key);
+                    Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
+                });
+        }
+
+        [Fact]
+        public async Task CustomValidationAttribute_IsValidated()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomeProperties),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString
+                    = new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseAttribute)}=1"));
+
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomeProperties>(result.Model);
+            Assert.Equal("1", model.NeverValidBecauseAttribute);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(1, modelState.ErrorCount);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseAttribute), kvp.Key);
+            var state = kvp.Value;
+            Assert.NotNull(state);
+            Assert.Equal(ModelValidationState.Invalid, state.ValidationState);
+            var error = Assert.Single(state.Errors);
+            Assert.Equal(
+                "'NeverValidBecauseAttribute' (display: 'Never valid') is not valid due to its associated NeverValidAttribute.",
+                error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+
+        [Fact]
+        public async Task ValidateNeverProperty_IsSkipped()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomeProperties),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString
+                    = new QueryString($"?{nameof(ValidateSomeProperties.ValidateNever)}=1"));
+
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomeProperties>(result.Model);
+            Assert.Equal("1", model.ValidateNever);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal(nameof(ValidateSomeProperties.ValidateNever), kvp.Key);
+            var state = kvp.Value;
+            Assert.NotNull(state);
+            Assert.Equal(ModelValidationState.Skipped, state.ValidationState);
+        }
+
+        [Fact]
+        public async Task ValidateNeverProperty_IsSkippedWithoutAccessingModel()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomeProperties),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomeProperties>(result.Model);
+
+            // Note this Exception is not thrown earlier.
+            Assert.Throws<NullReferenceException>(() => model.ValidateNeverLength);
+
+            Assert.True(modelState.IsValid);
+            Assert.Empty(modelState);
+        }
+
+        private class ValidateSometimesAttribute : Attribute, IPropertyValidationFilter
+        {
+            private readonly string _otherProperty;
+
+            public ValidateSometimesAttribute(string otherProperty)
+            {
+                // Would null-check otherProperty in real life.
+                _otherProperty = otherProperty;
+            }
+
+            public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
+            {
+                if (entry.Metadata.MetadataKind == ModelMetadataKind.Property &&
+                    parentEntry.Metadata != null)
+                {
+                    // In real life, would throw an InvalidOperationException if otherProperty were null i.e. the
+                    // property was not known. Could also assert container is non-null (see ValidationVisitor).
+                    var container = parentEntry.Model;
+                    var otherProperty = parentEntry.Metadata.Properties[_otherProperty];
+                    if (otherProperty.PropertyGetter(container) == null)
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+        }
+
+        private record ValidateSomePropertiesSometimes(string Control)
+        {
+            [ValidateSometimes(nameof(Control))]
+            [Range(0, 10)]
+            public int ControlLength => Control.Length;
+        }
+
+        [Fact]
+        public async Task PropertyToSometimesSkip_IsSkipped_IfControlIsNull()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomePropertiesSometimes),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
+            modelState.SetModelValue(
+                nameof(ValidateSomePropertiesSometimes.ControlLength),
+                rawValue: null,
+                attemptedValue: null);
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
+            Assert.Null(model.Control);
+
+            // Note this Exception is not thrown earlier.
+            Assert.Throws<NullReferenceException>(() => model.ControlLength);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), kvp.Key);
+            Assert.Equal(ModelValidationState.Skipped, kvp.Value.ValidationState);
+        }
+
+        [Fact]
+        public async Task PropertyToSometimesSkip_IsValidated_IfControlIsNotNull()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomePropertiesSometimes),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request => request.QueryString = new QueryString(
+                    $"?{nameof(ValidateSomePropertiesSometimes.Control)}=1"));
+
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
+            modelState.SetModelValue(
+                nameof(ValidateSomePropertiesSometimes.ControlLength),
+                rawValue: null,
+                attemptedValue: null);
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
+            Assert.Equal("1", model.Control);
+            Assert.Equal(1, model.ControlLength);
+
+            Assert.True(modelState.IsValid);
+            Assert.Collection(
+                modelState,
+                state => Assert.Equal(nameof(ValidateSomePropertiesSometimes.Control), state.Key),
+                state =>
+                {
+                    Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), state.Key);
+                    Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
+                });
+        }
+
+        // This type has a IPropertyValidationFilter declared on a property, but no validators.
+        // We should expect validation to short-circuit
+        private record ValidateSomePropertiesSometimesWithoutValidation(string Control)
+        {
+            [ValidateSometimes(nameof(Control))]
+            public int ControlLength => Control.Length;
+        }
+
+        [Fact]
+        public async Task PropertyToSometimesSkip_IsNotValidated_IfNoValidationAttributesExistButPropertyValidationFilterExists()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(ValidateSomePropertiesSometimesWithoutValidation),
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var modelState = testContext.ModelState;
+
+            // Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
+            modelState.SetModelValue(
+                nameof(ValidateSomePropertiesSometimes.ControlLength),
+                rawValue: null,
+                attemptedValue: null);
+
+            // Act
+            var result = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(result.IsModelSet);
+            var model = Assert.IsType<ValidateSomePropertiesSometimesWithoutValidation>(result.Model);
+            Assert.Null(model.Control);
+
+            // Note this Exception is not thrown earlier.
+            Assert.Throws<NullReferenceException>(() => model.ControlLength);
+
+            Assert.True(modelState.IsValid);
+            var kvp = Assert.Single(modelState);
+            Assert.Equal(nameof(ValidateSomePropertiesSometimesWithoutValidation.ControlLength), kvp.Key);
+            Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
+        }
+
+        private record Order11
+        (
+            IEnumerable<Address> ShippingAddresses,
+
+            Address HomeAddress,
+
+            [FromBody]
+            Address OfficeAddress
+        );
+
+        private record Address
+        (
+            int Street,
+
+            string State,
+
+            [Range(10000, 99999)]
+            int Zip,
+
+            Country Country
+        );
+
+        private record Country(string Name);
+
+        [Fact]
+        public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
+        {
+            // Arrange
+            var parameter = new ParameterDescriptor
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order11)
+            };
+
+            var input = "{\"Zip\":\"47\"}";
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request =>
+                {
+                    request.QueryString =
+                        new QueryString("?HomeAddress.Country.Name=US&ShippingAddresses[0].Zip=45&HomeAddress.Zip=46");
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
+                    request.ContentType = "application/json";
+                },
+                options =>
+                {
+                    options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Address)));
+                });
+
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            Assert.Equal(3, modelState.Count);
+            Assert.Equal(0, modelState.ErrorCount);
+            Assert.True(modelState.IsValid);
+
+            var entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Country.Name").Value;
+            Assert.Equal("US", entry.AttemptedValue);
+            Assert.Equal("US", entry.RawValue);
+            Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "ShippingAddresses[0].Zip").Value;
+            Assert.Equal("45", entry.AttemptedValue);
+            Assert.Equal("45", entry.RawValue);
+            Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Zip").Value;
+            Assert.Equal("46", entry.AttemptedValue);
+            Assert.Equal("46", entry.RawValue);
+            Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
+        }
+
+        [Fact]
+        public async Task FromBody_JToken_ExcludedFromValidation()
+        {
+            // Arrange
+            var options = new TestMvcOptions().Value;
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(options);
+            var parameter = new ParameterDescriptor
+            {
+                Name = "Parameter1",
+                BindingInfo = new BindingInfo
+                {
+                    BinderModelName = "CustomParameter",
+                    BindingSource = BindingSource.Body
+                },
+                ParameterType = typeof(JToken)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                updateRequest: request =>
+                {
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello\" }"));
+                    request.ContentType = "application/json";
+                },
+                mvcOptions: options);
+
+            var httpContext = testContext.HttpContext;
+            var modelState = testContext.ModelState;
+
+            // We need to add another model state entry which should get marked as skipped so
+            // we can prove that the JObject was skipped.
+            modelState.SetModelValue("CustomParameter.message", "Hello", "Hello");
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.NotNull(modelBindingResult.Model);
+            var message = Assert.IsType<JObject>(modelBindingResult.Model).GetValue("message").Value<string>();
+            Assert.Equal("Hello", message);
+
+            Assert.True(modelState.IsValid);
+            Assert.Single(modelState);
+
+            var entry = Assert.Single(modelState, kvp => kvp.Key == "CustomParameter.message");
+            Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState);
+        }
+
+        // Regression test for https://github.com/aspnet/Mvc/issues/3743
+        //
+        // A cancellation token that's bound with the empty prefix will end up suppressing
+        // the empty prefix. Since the empty prefix is a prefix of everything, this will
+        // basically result in clearing out all model errors, which is BAD.
+        //
+        // The fix is to treat non-user-input as have a key of null, which means that the MSD
+        // isn't even examined when it comes to suppressing validation.
+        [Fact]
+        public async Task CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
+            var parameter = new ParameterDescriptor
+            {
+                Name = "cancellationToken",
+                ParameterType = typeof(CancellationToken)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext();
+
+            var httpContext = testContext.HttpContext;
+            var modelState = testContext.ModelState;
+
+            // We need to add another model state entry - we want this to be ignored.
+            modelState.SetModelValue("message", "Hello", "Hello");
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.NotNull(modelBindingResult.Model);
+            Assert.IsType<CancellationToken>(modelBindingResult.Model);
+
+            Assert.False(modelState.IsValid);
+            Assert.Single(modelState);
+
+            var entry = Assert.Single(modelState, kvp => kvp.Key == "message");
+            Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
+        }
+
+        // Similar to CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors - binding the body
+        // with the empty prefix should not cause unrelated modelstate entries to get suppressed.
+        [Fact]
+        public async Task FromBody_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors_Valid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
+            var parameter = new ParameterDescriptor
+            {
+                Name = "Parameter1",
+                BindingInfo = new BindingInfo
+                {
+                    BindingSource = BindingSource.Body
+                },
+                ParameterType = typeof(Greeting)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request =>
+                {
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello\" }"));
+                    request.ContentType = "application/json";
+                });
+
+            var httpContext = testContext.HttpContext;
+            var modelState = testContext.ModelState;
+
+            // We need to add another model state entry which should not get changed.
+            modelState.SetModelValue("other.key", "1", "1");
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.NotNull(modelBindingResult.Model);
+            var message = Assert.IsType<Greeting>(modelBindingResult.Model).Message;
+            Assert.Equal("Hello", message);
+
+            Assert.False(modelState.IsValid);
+            Assert.Single(modelState);
+
+            var entry = Assert.Single(modelState, kvp => kvp.Key == "other.key");
+            Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
+        }
+
+        // Similar to CancellationToken_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors - binding the body
+        // with the empty prefix should not cause unrelated modelstate entries to get suppressed.
+        [Fact]
+        public async Task FromBody_WithEmptyPrefix_DoesNotSuppressUnrelatedErrors_Invalid()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(new TestMvcOptions().Value);
+            var parameter = new ParameterDescriptor
+            {
+                Name = "Parameter1",
+                BindingInfo = new BindingInfo
+                {
+                    BindingSource = BindingSource.Body
+                },
+                ParameterType = typeof(Greeting)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(
+                request =>
+                {
+                    // This string is too long and will have a validation error.
+                    request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello There\" }"));
+                    request.ContentType = "application/json";
+                });
+
+            var httpContext = testContext.HttpContext;
+            var modelState = testContext.ModelState;
+
+            // We need to add another model state entry which should not get changed.
+            modelState.SetModelValue("other.key", "1", "1");
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+            Assert.NotNull(modelBindingResult.Model);
+            var message = Assert.IsType<Greeting>(modelBindingResult.Model).Message;
+            Assert.Equal("Hello There", message);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(2, modelState.Count);
+
+            var entry = Assert.Single(modelState, kvp => kvp.Key == "Message");
+            Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
+
+            entry = Assert.Single(modelState, kvp => kvp.Key == "other.key");
+            Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
+        }
+
+        private record Greeting([StringLength(5)] string Message);
+
+        [Fact]
+        public async Task Validation_NoAttributeInGraphOfObjects_WithDefaultValidatorProviders()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Order12),
+                BindingInfo = new BindingInfo
+                {
+                    BindingSource = BindingSource.Body
+                },
+            };
+
+            var input = new Order12(10, new byte[40]);
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(input)));
+                request.ContentType = "application/json";
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Order12>(modelBindingResult.Model);
+            Assert.Equal(input.Id, model.Id);
+            Assert.Equal(input.OrderFile, model.OrderFile);
+            Assert.Null(model.RelatedOrders);
+
+            Assert.Empty(modelState);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+        }
+
+        private record Order12(int Id, byte[] OrderFile)
+        {
+            public IList<Order12> RelatedOrders { get; set; }
+        }
+
+        [Fact]
+        public async Task Validation_ListOfType_NoValidatorOnParameter()
+        {
+            // Arrange
+            var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_NoValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
+                .GetParameters()
+                .First();
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = parameterInfo.Name,
+                ParameterType = parameterInfo.ParameterType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?[0]=1&[1]=2");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<int>>(modelBindingResult.Model);
+            Assert.Equal(new[] { 1, 2 }, model);
+
+            Assert.False(modelMetadata.HasValidators);
+
+            Assert.True(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
+            Assert.Equal("1", entry.AttemptedValue);
+            Assert.Equal("1", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
+            Assert.Equal("2", entry.AttemptedValue);
+            Assert.Equal("2", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private static void Validation_ListOfType_NoValidatorOnParameterTestMethod(List<int> parameter) { }
+
+        [Fact]
+        public async Task Validation_ListOfType_ValidatorOnParameter()
+        {
+            // Arrange
+            var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_ValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
+                .GetParameters()
+                .First();
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = parameterInfo.Name,
+                ParameterType = parameterInfo.ParameterType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?[0]=1&[1]=2");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<List<int>>(modelBindingResult.Model);
+            Assert.Equal(new[] { 1, 2 }, model);
+
+            Assert.True(modelMetadata.HasValidators);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "").Value;
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
+            Assert.Equal("1", entry.AttemptedValue);
+            Assert.Equal("1", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
+            Assert.Equal("2", entry.AttemptedValue);
+            Assert.Equal("2", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private static void Validation_ListOfType_ValidatorOnParameterTestMethod([ConsistentMinLength(3)] List<int> parameter) { }
+
+        private class ConsistentMinLength : ValidationAttribute
+        {
+            private readonly int _length;
+
+            public ConsistentMinLength(int length)
+            {
+                _length = length;
+            }
+
+            public override bool IsValid(object value)
+            {
+                return value is ICollection collection && collection.Count >= _length;
+            }
+        }
+
+        [Fact]
+        public async Task Validation_CollectionOfType_ValidatorOnElement()
+        {
+            // Arrange
+            var parameterInfo = GetType().GetMethod(nameof(Validation_CollectionOfType_ValidatorOnElementTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
+                .GetParameters()
+                .First();
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = parameterInfo.Name,
+                ParameterType = parameterInfo.ParameterType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?p[0].Id=1&p[1].Id=2");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Collection<InvalidEvenIds>>(modelBindingResult.Model);
+            Assert.Equal(1, model[0].Id);
+            Assert.Equal(2, model[1].Id);
+
+            Assert.True(modelMetadata.HasValidators);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "p[0].Id").Value;
+            Assert.Equal("1", entry.AttemptedValue);
+            Assert.Equal("1", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "p[1]").Value;
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "p[1].Id").Value;
+            Assert.Equal("2", entry.AttemptedValue);
+            Assert.Equal("2", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private static void Validation_CollectionOfType_ValidatorOnElementTestMethod(Collection<InvalidEvenIds> p) { }
+
+        public class InvalidEvenIds : IValidatableObject
+        {
+            public int Id { get; set; }
+
+            public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+            {
+                if (Id % 2 == 0)
+                {
+                    yield return new ValidationResult("Failed validation");
+                }
+            }
+        }
+
+        [Fact]
+        public async Task Validation_DictionaryType_NoValidators()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(IDictionary<string, int>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value=10");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
+            Assert.Collection(
+                model.OrderBy(k => k.Key),
+                kvp =>
+                {
+                    Assert.Equal("key0", kvp.Key);
+                    Assert.Equal(10, kvp.Value);
+                });
+
+            Assert.True(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
+            Assert.Equal("10", entry.AttemptedValue);
+            Assert.Equal("10", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        [Fact]
+        public async Task Validation_DictionaryType_ValueHasValidators()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(Dictionary<string, NeverValid>)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value.NeverValidProperty=value0");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Dictionary<string, NeverValid>>(modelBindingResult.Model);
+            Assert.Collection(
+                model.OrderBy(k => k.Key),
+                kvp =>
+                {
+                    Assert.Equal("key0", kvp.Key);
+                    Assert.Equal("value0", kvp.Value.NeverValidProperty);
+                });
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
+            Assert.Equal("key0", entry.AttemptedValue);
+            Assert.Equal("key0", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value.NeverValidProperty").Value;
+            Assert.Equal("value0", entry.AttemptedValue);
+            Assert.Equal("value0", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+            Assert.Single(entry.Errors);
+        }
+
+        [Fact]
+        public async Task Validation_TopLevelProperty_NoValidation()
+        {
+            // Arrange
+            var modelType = typeof(Validation_TopLevelPropertyController);
+            var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelPropertyController.Model));
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = propertyInfo.Name,
+                ParameterType = propertyInfo.PropertyType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Model.Id=12");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
+            Assert.Equal(12, model.Id);
+
+            Assert.False(modelMetadata.HasValidators);
+
+            Assert.True(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
+            Assert.Equal("12", entry.AttemptedValue);
+            Assert.Equal("12", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        public record Validation_TopLevelPropertyModel(int Id);
+
+        private class Validation_TopLevelPropertyController
+        {
+            public Validation_TopLevelPropertyModel Model { get; set; }
+        }
+
+        [Fact]
+        public async Task Validation_TopLevelProperty_ValidationOnProperty()
+        {
+            // Arrange
+            var modelType = typeof(Validation_TopLevelProperty_ValidationOnPropertyController);
+            var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelProperty_ValidationOnPropertyController.Model));
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = propertyInfo.Name,
+                ParameterType = propertyInfo.PropertyType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Model.Id=12");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
+            Assert.Equal(12, model.Id);
+
+            Assert.True(modelMetadata.HasValidators);
+
+            Assert.False(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
+            Assert.Equal("12", entry.AttemptedValue);
+            Assert.Equal("12", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+
+            entry = Assert.Single(modelState, e => e.Key == "Model").Value;
+            Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
+        }
+
+        public class Validation_TopLevelProperty_ValidationOnPropertyController
+        {
+            [CustomValidation(typeof(Validation_TopLevelProperty_ValidationOnPropertyController), nameof(Validate))]
+            public Validation_TopLevelPropertyModel Model { get; set; }
+
+            public static ValidationResult Validate(ValidationContext context)
+            {
+                return new ValidationResult("Invalid result");
+            }
+        }
+
+        [Fact]
+        public async Task Validation_InfinitelyRecursiveType_NoValidators()
+        {
+            // Arrange
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+            var parameter = new ParameterDescriptor()
+            {
+                Name = "parameter",
+                ParameterType = typeof(RecursiveModel)
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Property1=8");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
+            Assert.Equal(8, model.Property1);
+
+            Assert.True(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
+            Assert.Equal("8", entry.AttemptedValue);
+            Assert.Equal("8", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private record RecursiveModel(int Property1)
+        {
+            public RecursiveModel Property2 { get; set; }
+
+            public RecursiveModel Property3 => new RecursiveModel(Property1);
+        }
+
+        [Fact]
+        public async Task Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameter()
+        {
+            // Arrange
+            var parameterInfo = GetType().GetMethod(nameof(Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod), BindingFlags.NonPublic | BindingFlags.Static)
+                .GetParameters()
+                .First();
+
+            var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
+            var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
+            var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
+
+            var parameter = new ParameterDescriptor()
+            {
+                Name = parameterInfo.Name,
+                ParameterType = parameterInfo.ParameterType,
+            };
+
+            var testContext = ModelBindingTestHelper.GetTestContext(request =>
+            {
+                request.QueryString = new QueryString("?Property1=8");
+            });
+
+            var modelState = testContext.ModelState;
+
+            // Act
+            var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
+
+            // Assert
+            Assert.True(modelBindingResult.IsModelSet);
+
+            var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
+            Assert.Equal(8, model.Property1);
+
+            Assert.True(modelState.IsValid);
+            Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
+
+            var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
+            Assert.Equal("8", entry.AttemptedValue);
+            Assert.Equal("8", entry.RawValue);
+            Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
+        }
+
+        private static void Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod([Required] RecursiveModel model) { }
+
+        private static void AssertRequiredError(string key, ModelError error)
+        {
+            Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage(key), error.ErrorMessage);
+            Assert.Null(error.Exception);
+        }
+    }
+}

+ 28 - 1
src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs

@@ -1,7 +1,10 @@
 // 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.
 
+using System;
 using System.Buffers;
+using System.ComponentModel.DataAnnotations;
+using System.Security.Cryptography.X509Certificates;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Formatters;
@@ -44,7 +47,7 @@ namespace FormatterWebSite.Controllers
         }
 
         [HttpPost]
-        public IActionResult ReturnInput([FromBody]DummyClass dummyObject)
+        public IActionResult ReturnInput([FromBody] DummyClass dummyObject)
         {
             if (!ModelState.IsValid)
             {
@@ -71,6 +74,9 @@ namespace FormatterWebSite.Controllers
             return model;
         }
 
+        [HttpPost]
+        public ActionResult<SimpleRecordModel> RoundtripRecordType([FromBody] SimpleRecordModel model) => model;
+
         public class SimpleModel
         {
             public int Id { get; set; }
@@ -79,5 +85,26 @@ namespace FormatterWebSite.Controllers
 
             public string StreetName { get; set; }
         }
+
+        public record SimpleRecordModel(int Id, string Name, string StreetName);
+
+        public record SimpleModelWithValidation(
+            [Range(1, 100)]
+            int Id,
+
+            [Required]
+            [StringLength(8, MinimumLength = 2)]
+            string Name,
+
+            [Required]
+            string StreetName);
+
+        [HttpPost]
+        public ActionResult<SimpleModelWithValidation> RoundtripModelWithValidation([FromBody] SimpleModelWithValidation model)
+        {
+            if (!ModelState.IsValid)
+                return ValidationProblem();
+            return model;
+        }
     }
 }

+ 2 - 1
src/Mvc/test/WebSites/FormatterWebSite/FormatterWebSite.csproj

@@ -1,7 +1,8 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <LangVersion>9.0</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>

+ 4 - 2
src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs

@@ -1,9 +1,10 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
 
 namespace FormatterWebSite
 {
@@ -15,6 +16,7 @@ namespace FormatterWebSite
             Value = identifier;
         }
 
+        [Required]
         public string Value { get; }
 
         public RecursiveIdentifier AccountIdentifier => new RecursiveIdentifier(Value);
@@ -24,4 +26,4 @@ namespace FormatterWebSite
             return Enumerable.Empty<ValidationResult>();
         }
     }
-}
+}

+ 5 - 0
src/Mvc/test/WebSites/HtmlGenerationWebSite/Areas/Customer/Controllers/HtmlGeneration_CustomerController.cs

@@ -12,5 +12,10 @@ namespace HtmlGenerationWebSite.Areas.Customer.Controllers
         {
             return View("Customer");
         }
+
+        public IActionResult CustomerWithRecords(Models.CustomerRecord customer)
+        {
+            return View("CustomerWithRecords");
+        }
     }
 }

+ 1 - 0
src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj

@@ -2,6 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <LangVersion>9.0</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>

+ 29 - 0
src/Mvc/test/WebSites/HtmlGenerationWebSite/Models/CustomerRecord.cs

@@ -0,0 +1,29 @@
+// 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.
+
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+
+namespace HtmlGenerationWebSite.Models
+{
+    public record CustomerRecord
+    (
+        [Range(1, 100)]
+        int Number,
+
+        string Name,
+
+        [Required]
+        string Password,
+
+        [EnumDataType(typeof(Gender))]
+        Gender Gender,
+
+        string PhoneNumber,
+
+        [DataType(DataType.EmailAddress)]
+        string Email,
+
+        string Key
+    );
+}

+ 46 - 0
src/Mvc/test/WebSites/HtmlGenerationWebSite/Views/Shared/CustomerWithRecords.cshtml

@@ -0,0 +1,46 @@
+@model HtmlGenerationWebSite.Models.CustomerRecord
+
+@{
+    ViewBag.Title = "Customer Page";
+}
+
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+
+<html>
+<body>
+    <form asp-route-area="Customer" asp-controller="HtmlGeneration_Customer" asp-action="Index">
+        <div id="Number">
+            <label asp-for="Number" class="order"></label>
+            <input asp-for="Number" type="number" class="form-control" />
+            <span asp-validation-for="Number"></span>
+        </div>
+        <div id="Name">
+            <label asp-for="Name" class="order"></label>
+            <input asp-for="Name" type="text" />
+        </div>
+        <div id="Email">
+            <label asp-for="Email" class="order"></label>
+            <input asp-for="Email" type="email" />
+            <span asp-validation-for="Email"></span>
+        </div>
+        <div id="PhoneNumber">
+            <label asp-for="PhoneNumber" class="order"></label>
+            <input asp-for="PhoneNumber" type="tel" />
+        </div>
+        <div id="Password">
+            <label asp-for="Password" class="order"></label>
+            <input asp-for="Password" type="password" class="form-control" />
+            <span asp-validation-for="Password"></span>
+        </div>
+        <div id="Gender">
+            <label asp-for="Gender" class="order"></label>
+            <input asp-for="Gender" type="radio" value="Male" /> Male
+            <input asp-for="Gender" type="radio" value="Female" /> Female
+            <span asp-validation-for="Gender"></span>
+        </div>
+        <div id="validation-summary-all" asp-validation-summary="All" class="order"></div>
+        <div id="validation-summary-model" asp-validation-summary="ModelOnly" class="order"></div>
+        <input type="submit"/>
+    </form>
+</body>
+</html>

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Properties/launchSettings.json

@@ -26,6 +26,7 @@
     },
     "BlazorServerWeb-CSharp": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(RequiresHttps)
       "applicationUrl": "https://localhost:5001;http://localhost:5000",

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Properties/launchSettings.json

@@ -27,6 +27,7 @@
     },
     "ComponentsWebAssembly-CSharp": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
       //#if(RequiresHttps)

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Properties/launchSettings.json

@@ -22,6 +22,7 @@
       },
       "ComponentsWebAssembly-CSharp.Server": {
         "commandName": "Project",
+        "dotnetRunMessages": "true",
         "launchBrowser": true,
         "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
         //#if(RequiresHttps)

+ 7 - 6
src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json

@@ -1,11 +1,11 @@
 {
   "iisSettings": {
-    //#if (WindowsAuth) 
-    "windowsAuthentication": true, 
-    "anonymousAuthentication": false, 
-    //#else 
-    "windowsAuthentication": false, 
-    "anonymousAuthentication": true, 
+    //#if (WindowsAuth)
+    "windowsAuthentication": true,
+    "anonymousAuthentication": false,
+    //#else
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
     //#endif
     "iisExpress": {
       "applicationUrl": "http://localhost:8080",
@@ -26,6 +26,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(NoHttps)
       "applicationUrl": "http://localhost:5000",

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/Properties/launchSettings.json

@@ -21,6 +21,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(NoHttps)
       "applicationUrl": "http://localhost:5000",

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/Properties/launchSettings.json

@@ -2,6 +2,7 @@
   "profiles": {
     "GrpcService-CSharp": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": false,
       "applicationUrl": "http://localhost:5000;https://localhost:5001",
       "environmentVariables": {

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Properties/launchSettings.json

@@ -31,6 +31,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(RequiresHttps)
       "applicationUrl": "https://localhost:5001;http://localhost:5000",

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Properties/launchSettings.json

@@ -31,6 +31,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(RequiresHttps)
       "applicationUrl": "https://localhost:5001;http://localhost:5000",

+ 8 - 7
src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/Properties/launchSettings.json

@@ -1,12 +1,12 @@
 {
   "iisSettings": {
-    //#if (WindowsAuth) 
-    "windowsAuthentication": true, 
-    "anonymousAuthentication": false, 
-    //#else 
-    "windowsAuthentication": false, 
-    "anonymousAuthentication": true, 
-    //#endif 
+    //#if (WindowsAuth)
+    "windowsAuthentication": true,
+    "anonymousAuthentication": false,
+    //#else
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    //#endif
     "iisExpress": {
       "applicationUrl": "http://localhost:8080",
       //#if(NoHttps)
@@ -26,6 +26,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       //#if(NoHttps)
       "applicationUrl": "http://localhost:5000",

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Properties/launchSettings.json

@@ -28,6 +28,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       "launchUrl": "weatherforecast",
       //#if(RequiresHttps)

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/Properties/launchSettings.json

@@ -28,6 +28,7 @@
     },
     "Company.WebApplication1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "launchBrowser": true,
       "launchUrl": "weatherforecast",
       //#if(NoHttps)

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/Properties/launchSettings.json

@@ -2,6 +2,7 @@
   "profiles": {
     "Company.Application1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "environmentVariables": {
         "DOTNET_ENVIRONMENT": "Development"
       }

+ 1 - 0
src/ProjectTemplates/Web.ProjectTemplates/content/Worker-FSharp/Properties/launchSettings.json

@@ -2,6 +2,7 @@
   "profiles": {
     "Company.Application1": {
       "commandName": "Project",
+      "dotnetRunMessages": "true",
       "environmentVariables": {
         "DOTNET_ENVIRONMENT": "Development"
       }

+ 2 - 38
src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs

@@ -28,7 +28,7 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <param name="authenticationScheme"></param>
         /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
         public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme)
-            => builder.AddCertificate(authenticationScheme, configureOptions: (Action<CertificateAuthenticationOptions, IServiceProvider>)null);
+            => builder.AddCertificate(authenticationScheme, configureOptions: null);
 
         /// <summary>
         /// Adds certificate authentication.
@@ -39,16 +39,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions> configureOptions)
             => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
 
-        /// <summary>
-        /// Adds certificate authentication.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
-        /// <param name="configureOptions"></param>
-        /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
-        public static AuthenticationBuilder AddCertificate<TService>(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
-            => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions);
-
         /// <summary>
         /// Adds certificate authentication.
         /// </summary>
@@ -60,33 +50,7 @@ namespace Microsoft.Extensions.DependencyInjection
             this AuthenticationBuilder builder,
             string authenticationScheme,
             Action<CertificateAuthenticationOptions> configureOptions)
-        {
-            Action<CertificateAuthenticationOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddCertificate(authenticationScheme, configureOptionsWithServices);
-        }
-
-        /// <summary>
-        /// Adds certificate authentication.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
-        /// <param name="authenticationScheme"></param>
-        /// <param name="configureOptions"></param>
-        /// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
-        public static AuthenticationBuilder AddCertificate<TService>(
-            this AuthenticationBuilder builder,
-            string authenticationScheme,
-            Action<CertificateAuthenticationOptions, TService> configureOptions) where TService : class
-            => builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler, TService>(authenticationScheme, configureOptions);
+            => builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler>(authenticationScheme, configureOptions);
 
         /// <summary>
         /// Adds certificate authentication.

+ 4 - 3
src/Security/Authentication/Cookies/samples/CookieSessionSample/Startup.cs

@@ -14,14 +14,15 @@ namespace CookieSessionSample
     {
         public void ConfigureServices(IServiceCollection services)
         {
-            services.AddSingleton<MemoryCacheTicketStore>();
-
             // This can be removed after https://github.com/aspnet/IISIntegration/issues/371
             services.AddAuthentication(options =>
             {
                 options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                 options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
-            }).AddCookie<MemoryCacheTicketStore>((o, ticketStore) => o.SessionStore = ticketStore);
+            }).AddCookie();
+            
+            services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
+                .Configure<MemoryCacheTicketStore>((o, ticketStore) => o.SessionStore = ticketStore);
         }
 
         public void Configure(IApplicationBuilder app)

+ 3 - 24
src/Security/Authentication/Cookies/src/CookieExtensions.cs

@@ -15,40 +15,19 @@ namespace Microsoft.Extensions.DependencyInjection
             => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
 
         public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme)
-            => builder.AddCookie(authenticationScheme, configureOptions: (Action<CookieAuthenticationOptions, IServiceProvider>)null);
+            => builder.AddCookie(authenticationScheme, configureOptions: null);
 
-        public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
-            => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
-
-        public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
+        public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions) 
             => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
 
         public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions> configureOptions)
             => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
 
-        public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
-            => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
-
         public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
-        {
-            Action<CookieAuthenticationOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddCookie(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddCookie<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
             builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
-            return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 9 - 86
src/Security/Authentication/Core/src/AuthenticationBuilder.cs

@@ -25,31 +25,25 @@ namespace Microsoft.AspNetCore.Authentication
         /// </summary>
         public virtual IServiceCollection Services { get; }
 
-        private AuthenticationBuilder AddSchemeHelper<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
+        private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
             where TOptions : AuthenticationSchemeOptions, new()
             where THandler : class, IAuthenticationHandler
         {
             Services.Configure<AuthenticationOptions>(o =>
             {
-                o.AddScheme(authenticationScheme, scheme =>
-                {
+                o.AddScheme(authenticationScheme, scheme => {
                     scheme.HandlerType = typeof(THandler);
                     scheme.DisplayName = displayName;
                 });
             });
-
-            var optionsBuilder = Services.AddOptions<TOptions>(authenticationScheme)
-                .Validate(o =>
-                {
-                    o.Validate(authenticationScheme);
-                    return true;
-                });
-
             if (configureOptions != null)
             {
-                optionsBuilder.Configure(configureOptions);
+                Services.Configure(authenticationScheme, configureOptions);
             }
-
+            Services.AddOptions<TOptions>(authenticationScheme).Validate(o => {
+                o.Validate(authenticationScheme);
+                return true;
+            });
             Services.AddTransient<THandler>();
             return this;
         }
@@ -66,22 +60,7 @@ namespace Microsoft.AspNetCore.Authentication
         public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
             where TOptions : AuthenticationSchemeOptions, new()
             where THandler : AuthenticationHandler<TOptions>
-            => AddSchemeHelper<TOptions, THandler, IServiceProvider>(authenticationScheme, displayName, MapConfiguration(configureOptions));
-
-        /// <summary>
-        /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
-        /// </summary>
-        /// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
-        /// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="authenticationScheme">The name of this scheme.</param>
-        /// <param name="displayName">The display name of this scheme.</param>
-        /// <param name="configureOptions">Used to configure the scheme options.</param>
-        /// <returns>The builder.</returns>
-        public virtual AuthenticationBuilder AddScheme<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
-            where TOptions : AuthenticationSchemeOptions, new()
-            where THandler : AuthenticationHandler<TOptions>
-            => AddSchemeHelper<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions);
+            => AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
 
         /// <summary>
         /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
@@ -96,20 +75,6 @@ namespace Microsoft.AspNetCore.Authentication
             where THandler : AuthenticationHandler<TOptions>
             => AddScheme<TOptions, THandler>(authenticationScheme, displayName: null, configureOptions: configureOptions);
 
-        /// <summary>
-        /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
-        /// </summary>
-        /// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
-        /// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="authenticationScheme">The name of this scheme.</param>
-        /// <param name="configureOptions">Used to configure the scheme options.</param>
-        /// <returns>The builder.</returns>
-        public virtual AuthenticationBuilder AddScheme<TOptions, THandler, TService>(string authenticationScheme, Action<TOptions, TService> configureOptions) where TService : class
-            where TOptions : AuthenticationSchemeOptions, new()
-            where THandler : AuthenticationHandler<TOptions>
-            => AddScheme<TOptions, THandler, TService>(authenticationScheme, displayName: null, configureOptions: configureOptions);
-
         /// <summary>
         /// Adds a <see cref="RemoteAuthenticationHandler{TOptions}"/> based <see cref="AuthenticationScheme"/> that supports remote authentication
         /// which can be used by <see cref="IAuthenticationService"/>.
@@ -128,25 +93,6 @@ namespace Microsoft.AspNetCore.Authentication
             return AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions: configureOptions);
         }
 
-        /// <summary>
-        /// Adds a <see cref="RemoteAuthenticationHandler{TOptions}"/> based <see cref="AuthenticationScheme"/> that supports remote authentication
-        /// which can be used by <see cref="IAuthenticationService"/>.
-        /// </summary>
-        /// <typeparam name="TOptions">The <see cref="RemoteAuthenticationOptions"/> type to configure the handler."/>.</typeparam>
-        /// <typeparam name="THandler">The <see cref="RemoteAuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="authenticationScheme">The name of this scheme.</param>
-        /// <param name="displayName">The display name of this scheme.</param>
-        /// <param name="configureOptions">Used to configure the scheme options.</param>
-        /// <returns>The builder.</returns>
-        public virtual AuthenticationBuilder AddRemoteScheme<TOptions, THandler, TService>(string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions) where TService : class
-            where TOptions : RemoteAuthenticationOptions, new()
-            where THandler : RemoteAuthenticationHandler<TOptions>
-        {
-            Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
-            return AddScheme<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions: configureOptions);
-        }
-
         /// <summary>
         /// Adds a <see cref="PolicySchemeHandler"/> based authentication handler which can be used to 
         /// redirect to other authentication schemes.
@@ -156,30 +102,7 @@ namespace Microsoft.AspNetCore.Authentication
         /// <param name="configureOptions">Used to configure the scheme options.</param>
         /// <returns>The builder.</returns>
         public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action<PolicySchemeOptions> configureOptions)
-            => AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler, IServiceProvider>(authenticationScheme, displayName, MapConfiguration(configureOptions));
-
-        /// <summary>
-        /// Adds a <see cref="PolicySchemeHandler"/> based authentication handler which can be used to 
-        /// redirect to other authentication schemes.
-        /// </summary>
-        /// <param name="authenticationScheme">The name of this scheme.</param>
-        /// <param name="displayName">The display name of this scheme.</param>
-        /// <param name="configureOptions">Used to configure the scheme options.</param>
-        /// <returns>The builder.</returns>
-        public virtual AuthenticationBuilder AddPolicyScheme<TService>(string authenticationScheme, string displayName, Action<PolicySchemeOptions, TService> configureOptions) where TService : class
-            => AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler, TService>(authenticationScheme, displayName, configureOptions);
-
-        private Action<TOptions, IServiceProvider> MapConfiguration<TOptions>(Action<TOptions> configureOptions)
-        {
-            if (configureOptions == null)
-            {
-                return null;
-            }
-            else
-            {
-                return (options, _) => configureOptions(options);
-            }
-        }
+            => AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler>(authenticationScheme, displayName, configureOptions);
 
         // Used to ensure that there's always a default sign in scheme that's not itself
         private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions

+ 1 - 22
src/Security/Authentication/Facebook/src/FacebookExtensions.cs

@@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action<FacebookOptions> configureOptions)
             => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, Action<FacebookOptions, TService> configureOptions) where TService : class
-            => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action<FacebookOptions> configureOptions)
             => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<FacebookOptions, TService> configureOptions) where TService : class
-            => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions> configureOptions)
-        {
-            Action<FacebookOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddFacebook(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddFacebook<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions, TService> configureOptions) where TService : class
-            => builder.AddOAuth<FacebookOptions, FacebookHandler, TService>(authenticationScheme, displayName, configureOptions);
+            => builder.AddOAuth<FacebookOptions, FacebookHandler>(authenticationScheme, displayName, configureOptions);
     }
 }

+ 1 - 22
src/Security/Authentication/Google/src/GoogleExtensions.cs

@@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action<GoogleOptions> configureOptions)
             => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, Action<GoogleOptions, TService> configureOptions) where TService : class
-            => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action<GoogleOptions> configureOptions)
             => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<GoogleOptions, TService> configureOptions) where TService : class
-            => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions> configureOptions)
-        {
-            Action<GoogleOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddGoogle(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddGoogle<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions, TService> configureOptions) where TService : class
-            => builder.AddOAuth<GoogleOptions, GoogleHandler, TService>(authenticationScheme, displayName, configureOptions);
+            => builder.AddOAuth<GoogleOptions, GoogleHandler>(authenticationScheme, displayName, configureOptions);
     }
 }

+ 1 - 22
src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs

@@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action<JwtBearerOptions> configureOptions)
             => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, Action<JwtBearerOptions, TService> configureOptions) where TService : class
-            => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> configureOptions)
             => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);
 
-        public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions, TService> configureOptions) where TService : class
-            => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);
-
         public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
-        {
-            Action<JwtBearerOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddJwtBearer(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddJwtBearer<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
-            return builder.AddScheme<JwtBearerOptions, JwtBearerHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 2 - 23
src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountExtensions.cs

@@ -15,31 +15,10 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action<MicrosoftAccountOptions> configureOptions)
             => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
-            => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action<MicrosoftAccountOptions> configureOptions)
             => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
-            => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions> configureOptions)
-        {
-            Action<MicrosoftAccountOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddMicrosoftAccount(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddMicrosoftAccount<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions, TService> configureOptions) where TService : class
-            => builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler, TService>(authenticationScheme, displayName, configureOptions);
+            => builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler>(authenticationScheme, displayName, configureOptions);
     }
-}
+}

+ 1 - 46
src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs

@@ -31,16 +31,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, Action<NegotiateOptions> configureOptions)
             => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions);
 
-        /// <summary>
-        /// Adds and configures Negotiate authentication.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
-        /// <param name="configureOptions">Allows for configuring the authentication handler.</param>
-        /// <returns>The original builder.</returns>
-        public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, Action<NegotiateOptions, TService> configureOptions) where TService : class
-            => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions);
-
         /// <summary>
         /// Adds and configures Negotiate authentication.
         /// </summary>
@@ -51,17 +41,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, Action<NegotiateOptions> configureOptions)
             => builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions);
 
-        /// <summary>
-        /// Adds and configures Negotiate authentication.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
-        /// <param name="authenticationScheme">The scheme name used to identify the authentication handler internally.</param>
-        /// <param name="configureOptions">Allows for configuring the authentication handler.</param>
-        /// <returns>The original builder.</returns>
-        public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<NegotiateOptions, TService> configureOptions) where TService : class
-            => builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions);
-
         /// <summary>
         /// Adds and configures Negotiate authentication.
         /// </summary>
@@ -71,33 +50,9 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <param name="configureOptions">Allows for configuring the authentication handler.</param>
         /// <returns>The original builder.</returns>
         public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions> configureOptions)
-        {
-            Action<NegotiateOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddNegotiate(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        /// <summary>
-        /// Adds and configures Negotiate authentication.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
-        /// <param name="authenticationScheme">The scheme name used to identify the authentication handler internally.</param>
-        /// <param name="displayName">The name displayed to users when selecting an authentication handler. The default is null to prevent this from displaying.</param>
-        /// <param name="configureOptions">Allows for configuring the authentication handler.</param>
-        /// <returns>The original builder.</returns>
-        public static AuthenticationBuilder AddNegotiate<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<NegotiateOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<NegotiateOptions>, PostConfigureNegotiateOptions>());
-            return builder.AddScheme<NegotiateOptions, NegotiateHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddScheme<NegotiateOptions, NegotiateHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 1 - 31
src/Security/Authentication/OAuth/src/OAuthExtensions.cs

@@ -14,50 +14,20 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions> configureOptions)
             => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddOAuth<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions, TService> configureOptions) where TService : class
-            => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>, TService>(authenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions> configureOptions)
             => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, displayName, configureOptions);
 
-        public static AuthenticationBuilder AddOAuth<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions, TService> configureOptions) where TService : class
-            => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>, TService>(authenticationScheme, displayName, configureOptions);
-
         public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions> configureOptions)
             where TOptions : OAuthOptions, new()
             where THandler : OAuthHandler<TOptions>
             => builder.AddOAuth<TOptions, THandler>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddOAuth<TOptions, THandler, TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions, TService> configureOptions)
-            where TOptions : OAuthOptions, new()
-            where THandler : OAuthHandler<TOptions>
-            where TService : class
-            => builder.AddOAuth<TOptions, THandler, TService>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
             where TOptions : OAuthOptions, new()
             where THandler : OAuthHandler<TOptions>
-        {
-            Action<TOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddOAuth<TOptions, THandler, IServiceProvider>(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddOAuth<TOptions, THandler, TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions, TService> configureOptions)
-            where TOptions : OAuthOptions, new()
-            where THandler : OAuthHandler<TOptions>
-            where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
-            return builder.AddRemoteScheme<TOptions, THandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 1 - 22
src/Security/Authentication/OpenIdConnect/src/OpenIdConnectExtensions.cs

@@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions)
             => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
-            => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions)
             => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
-            => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
-        {
-            Action<OpenIdConnectOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddOpenIdConnect(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddOpenIdConnect<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
-            return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 1 - 22
src/Security/Authentication/Twitter/src/TwitterExtensions.cs

@@ -17,34 +17,13 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action<TwitterOptions> configureOptions)
             => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions);
 
-        public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, Action<TwitterOptions, TService> configureOptions) where TService : class
-            => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions);
-
         public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action<TwitterOptions> configureOptions)
             => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions);
 
-        public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<TwitterOptions, TService> configureOptions) where TService : class
-            => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions);
-
         public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions> configureOptions)
-        {
-            Action<TwitterOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddTwitter(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        public static AuthenticationBuilder AddTwitter<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TwitterOptions>, TwitterPostConfigureOptions>());
-            return builder.AddRemoteScheme<TwitterOptions, TwitterHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddRemoteScheme<TwitterOptions, TwitterHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 1 - 46
src/Security/Authentication/WsFederation/src/WsFederationExtensions.cs

@@ -31,16 +31,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action<WsFederationOptions> configureOptions)
             => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions);
 
-        /// <summary>
-        /// Registers the <see cref="WsFederationHandler"/> using the default authentication scheme, display name, and the given options configuration.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder"></param>
-        /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
-        /// <returns></returns>
-        public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, Action<WsFederationOptions, TService> configureOptions) where TService : class
-            => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions);
-
         /// <summary>
         /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, default display name, and the given options configuration.
         /// </summary>
@@ -51,17 +41,6 @@ namespace Microsoft.Extensions.DependencyInjection
         public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action<WsFederationOptions> configureOptions)
             => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions);
 
-        /// <summary>
-        /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, default display name, and the given options configuration.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder"></param>
-        /// <param name="authenticationScheme"></param>
-        /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
-        /// <returns></returns>
-        public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, string authenticationScheme, Action<WsFederationOptions, TService> configureOptions) where TService : class
-            => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions);
-
         /// <summary>
         /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, display name, and options configuration.
         /// </summary>
@@ -71,33 +50,9 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
         /// <returns></returns>
         public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WsFederationOptions> configureOptions)
-        {
-            Action<WsFederationOptions, IServiceProvider> configureOptionsWithServices;
-            if (configureOptions == null)
-            {
-                configureOptionsWithServices = null;
-            }
-            else
-            {
-                configureOptionsWithServices = (options, _) => configureOptions(options);
-            }
-
-            return builder.AddWsFederation(authenticationScheme, displayName, configureOptionsWithServices);
-        }
-
-        /// <summary>
-        /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, display name, and options configuration.
-        /// </summary>
-        /// <typeparam name="TService">TService: A service resolved from the IServiceProvider for use when configuring this authentication provider. If you need multiple services then specify IServiceProvider and resolve them directly.</typeparam>
-        /// <param name="builder"></param>
-        /// <param name="authenticationScheme"></param>
-        /// <param name="displayName"></param>
-        /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
-        /// <returns></returns>
-        public static AuthenticationBuilder AddWsFederation<TService>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WsFederationOptions, TService> configureOptions) where TService : class
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<WsFederationOptions>, WsFederationPostConfigureOptions>());
-            return builder.AddRemoteScheme<WsFederationOptions, WsFederationHandler, TService>(authenticationScheme, displayName, configureOptions);
+            return builder.AddRemoteScheme<WsFederationOptions, WsFederationHandler>(authenticationScheme, displayName, configureOptions);
         }
     }
 }

+ 108 - 0
src/Tools/dotnet-watch/src/BrowserRefreshServer.cs

@@ -0,0 +1,108 @@
+// 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.
+
+using System;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.DotNet.Watcher.Tools
+{
+    public class BrowserRefreshServer : IAsyncDisposable
+    {
+        private readonly IReporter _reporter;
+        private readonly TaskCompletionSource _taskCompletionSource;
+        private IHost _refreshServer;
+        private WebSocket _webSocket;
+
+        public BrowserRefreshServer(IReporter reporter)
+        {
+            _reporter = reporter;
+            _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+        }
+
+        public async ValueTask<string> StartAsync(CancellationToken cancellationToken)
+        {
+            _refreshServer = new HostBuilder()
+                .ConfigureWebHost(builder =>
+                {
+                    builder.UseKestrel();
+                    builder.UseUrls("http://127.0.0.1:0");
+
+                    builder.Configure(app =>
+                    {
+                        app.UseWebSockets();
+                        app.Run(WebSocketRequest);
+                    });
+                })
+                .Build();
+
+            await _refreshServer.StartAsync(cancellationToken);
+
+            var serverUrl = _refreshServer.Services
+                .GetRequiredService<IServer>()
+                .Features
+                .Get<IServerAddressesFeature>()
+                .Addresses
+                .First();
+
+            return serverUrl.Replace("http://", "ws://");
+        }
+
+        private async Task WebSocketRequest(HttpContext context)
+        {
+            if (!context.WebSockets.IsWebSocketRequest)
+            {
+                context.Response.StatusCode = 400;
+                return;
+            }
+
+            _webSocket = await context.WebSockets.AcceptWebSocketAsync();
+            await _taskCompletionSource.Task;
+        }
+
+        public async Task SendMessage(byte[] messageBytes, CancellationToken cancellationToken = default)
+        {
+            if (_webSocket == null || _webSocket.CloseStatus.HasValue)
+            {
+                return;
+            }
+
+            try
+            {
+                await _webSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
+            }
+            catch (Exception ex)
+            {
+                _reporter.Output($"Refresh server error: {ex}");
+            }
+        }
+
+        public async ValueTask DisposeAsync()
+        {
+            if (_webSocket != null)
+            {
+                await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default);
+                _webSocket.Dispose();
+            }
+
+            if (_refreshServer != null)
+            {
+                await _refreshServer.StopAsync();
+                _refreshServer.Dispose();
+            }
+
+            _taskCompletionSource.TrySetResult();
+        }
+    }
+}

+ 18 - 1
src/Tools/dotnet-watch/src/DotNetWatcher.cs

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Tools.Internal;
 
 namespace Microsoft.DotNet.Watcher
 {
-    public class DotNetWatcher
+    public class DotNetWatcher : IAsyncDisposable
     {
         private readonly IReporter _reporter;
         private readonly ProcessRunner _processRunner;
@@ -31,6 +31,7 @@ namespace Microsoft.DotNet.Watcher
             {
                 new MSBuildEvaluationFilter(fileSetFactory),
                 new NoRestoreFilter(),
+                new LaunchBrowserFilter(),
             };
         }
 
@@ -140,5 +141,21 @@ namespace Microsoft.DotNet.Watcher
                 }
             }
         }
+
+        public async ValueTask DisposeAsync()
+        {
+            foreach (var filter in _filters)
+            {
+                if (filter is IAsyncDisposable asyncDisposable)
+                {
+                    await asyncDisposable.DisposeAsync();
+                }
+                else if (filter is IDisposable diposable)
+                {
+                    diposable.Dispose();
+                }
+                
+            }
+        }
     }
 }

+ 3 - 1
src/Tools/dotnet-watch/src/IFileSet.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System.Collections.Generic;
@@ -7,6 +7,8 @@ namespace Microsoft.DotNet.Watcher
 {
     public interface IFileSet : IEnumerable<string>
     {
+        bool IsNetCoreApp31OrNewer { get; }
+
         bool Contains(string filePath);
     }
 }

+ 5 - 2
src/Tools/dotnet-watch/src/Internal/FileSet.cs

@@ -11,8 +11,9 @@ namespace Microsoft.DotNet.Watcher.Internal
     {
         private readonly HashSet<string> _files;
 
-        public FileSet(IEnumerable<string> files)
+        public FileSet(bool isNetCoreApp31OrNewer, IEnumerable<string> files)
         {
+            IsNetCoreApp31OrNewer = isNetCoreApp31OrNewer;
             _files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
         }
 
@@ -20,7 +21,9 @@ namespace Microsoft.DotNet.Watcher.Internal
 
         public int Count => _files.Count;
 
-        public static IFileSet Empty = new FileSet(Array.Empty<string>());
+        public bool IsNetCoreApp31OrNewer { get; }
+
+        public static IFileSet Empty = new FileSet(false, Array.Empty<string>());
 
         public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
         IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();

+ 7 - 2
src/Tools/dotnet-watch/src/Internal/MsBuildFileSetFactory.cs

@@ -72,6 +72,7 @@ namespace Microsoft.DotNet.Watcher.Internal
                         Arguments = new[]
                         {
                             "msbuild",
+                            "/nologo",
                             _projectFile,
                             $"/p:_DotNetWatchListFile={watchList}"
                         }.Concat(_buildFlags),
@@ -84,8 +85,12 @@ namespace Microsoft.DotNet.Watcher.Internal
 
                     if (exitCode == 0 && File.Exists(watchList))
                     {
+                        var lines = File.ReadAllLines(watchList);
+                        var isNetCoreApp31OrNewer = lines.FirstOrDefault() == "true";
+
                         var fileset = new FileSet(
-                            File.ReadAllLines(watchList)
+                            isNetCoreApp31OrNewer,
+                            lines.Skip(1)
                                 .Select(l => l?.Trim())
                                 .Where(l => !string.IsNullOrEmpty(l)));
 
@@ -123,7 +128,7 @@ namespace Microsoft.DotNet.Watcher.Internal
                     {
                         _reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
 
-                        var fileSet = new FileSet(new[] { _projectFile });
+                        var fileSet = new FileSet(false, new[] { _projectFile });
 
                         using (var watcher = new FileSetWatcher(fileSet, _reporter))
                         {

+ 29 - 17
src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs

@@ -37,37 +37,49 @@ namespace Microsoft.DotNet.Watcher.Internal
             {
                 cancellationToken.Register(() => processState.TryKill());
 
-                process.OutputDataReceived += (_, a) =>
+                var readOutput = false;
+                var readError = false;
+                if (processSpec.IsOutputCaptured)
                 {
-                    if (!string.IsNullOrEmpty(a.Data))
+                    readOutput = true;
+                    readError = true;
+                    process.OutputDataReceived += (_, a) =>
                     {
-                        processSpec.OutputCapture.AddLine(a.Data);
-                    }
-                };
-                process.ErrorDataReceived += (_, a) =>
-                {
-                    if (!string.IsNullOrEmpty(a.Data))
+                        if (!string.IsNullOrEmpty(a.Data))
+                        {
+                            processSpec.OutputCapture.AddLine(a.Data);
+                        }
+                    };
+                    process.ErrorDataReceived += (_, a) =>
                     {
-                        processSpec.OutputCapture.AddLine(a.Data);
-                    }
-                };
+                        if (!string.IsNullOrEmpty(a.Data))
+                        {
+                            processSpec.OutputCapture.AddLine(a.Data);
+                        }
+                    };
+                }
+                else if (processSpec.OnOutput != null)
+                {
+                    readOutput = true;
+                    process.OutputDataReceived += processSpec.OnOutput;
+                }
 
                 stopwatch.Start();
                 process.Start();
 
                 _reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
 
-                if (processSpec.IsOutputCaptured)
+                if (readOutput)
                 {
-                    process.BeginErrorReadLine();
                     process.BeginOutputReadLine();
-                    await processState.Task;
                 }
-                else
+                if (readError)
                 {
-                    await processState.Task;
+                    process.BeginErrorReadLine();
                 }
 
+                await processState.Task;
+
                 exitCode = process.ExitCode;
                 stopwatch.Stop();
                 _reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
@@ -87,7 +99,7 @@ namespace Microsoft.DotNet.Watcher.Internal
                     Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments),
                     UseShellExecute = false,
                     WorkingDirectory = processSpec.WorkingDirectory,
-                    RedirectStandardOutput = processSpec.IsOutputCaptured,
+                    RedirectStandardOutput = processSpec.IsOutputCaptured || (processSpec.OnOutput != null),
                     RedirectStandardError = processSpec.IsOutputCaptured,
                 }
             };

+ 219 - 0
src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs

@@ -0,0 +1,219 @@
+// 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.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.DotNet.Watcher.Tools
+{
+    public sealed class LaunchBrowserFilter : IWatchFilter, IAsyncDisposable
+    {
+        private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload");
+        private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait");
+        private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None | RegexOptions.Compiled, TimeSpan.FromSeconds(10));
+
+        private readonly bool _runningInTest;
+        private readonly bool _suppressLaunchBrowser;
+        private readonly string _browserPath;
+        private bool _canLaunchBrowser;
+        private Process _browserProcess;
+        private bool _browserLaunched;
+        private BrowserRefreshServer _refreshServer;
+        private IReporter _reporter;
+        private string _launchPath;
+        private CancellationToken _cancellationToken;
+
+        public LaunchBrowserFilter()
+        {
+            var suppressLaunchBrowser = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER");
+            _suppressLaunchBrowser = (suppressLaunchBrowser == "1" || suppressLaunchBrowser == "true");
+            _runningInTest = Environment.GetEnvironmentVariable("__DOTNET_WATCH_RUNNING_AS_TEST") == "true";
+            _browserPath = Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH");
+        }
+
+        public async ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
+        {
+            if (_suppressLaunchBrowser)
+            {
+                return;
+            }
+
+            if (context.Iteration == 0)
+            {
+                _reporter = context.Reporter;
+
+                if (CanLaunchBrowser(context, out var launchPath))
+                {
+                    context.Reporter.Verbose("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.");
+                    _canLaunchBrowser = true;
+                    _launchPath = launchPath;
+                    _cancellationToken = cancellationToken;
+
+                    _refreshServer = new BrowserRefreshServer(context.Reporter);
+                    var serverUrl = await _refreshServer.StartAsync(cancellationToken);
+
+                    context.Reporter.Verbose($"Refresh server running at {serverUrl}.");
+                    context.ProcessSpec.EnvironmentVariables["DOTNET_WATCH_REFRESH_URL"] = serverUrl;
+
+                    context.ProcessSpec.OnOutput += OnOutput;
+                }
+            }
+
+            if (_canLaunchBrowser)
+            {
+                if (context.Iteration > 0)
+                {
+                    // We've detected a change. Notify the browser.
+                    await _refreshServer.SendMessage(WaitMessage, cancellationToken);
+                }
+            }
+        }
+
+        private void OnOutput(object sender, DataReceivedEventArgs eventArgs)
+        {
+            // We've redirected the output, but want to ensure that it continues to appear in the user's console.
+            Console.WriteLine(eventArgs.Data);
+
+            if (string.IsNullOrEmpty(eventArgs.Data))
+            {
+                return;
+            }
+
+            var match = NowListeningRegex.Match(eventArgs.Data);
+            if (match.Success)
+            {
+                var launchUrl = match.Groups["url"].Value;
+
+                var process = (Process)sender;
+                process.OutputDataReceived -= OnOutput;
+                process.CancelOutputRead();
+
+                if (!_browserLaunched)
+                {
+                    _reporter.Verbose("Launching browser.");
+                    try
+                    {
+                        LaunchBrowser(launchUrl);
+                        _browserLaunched = true;
+                    }
+                    catch (Exception ex)
+                    {
+                        _reporter.Output($"Unable to launch browser: {ex}");
+                        _canLaunchBrowser = false;
+                    }
+                }
+                else
+                {
+                    _reporter.Verbose("Reloading browser.");
+                    _ = _refreshServer.SendMessage(ReloadMessage, _cancellationToken);
+                }
+            }
+        }
+
+        private void LaunchBrowser(string launchUrl)
+        {
+            var fileName = launchUrl + "/" + _launchPath;
+            var args = string.Empty;
+            if (!string.IsNullOrEmpty(_browserPath))
+            {
+                args = fileName;
+                fileName = _browserPath;
+            }
+
+            if (_runningInTest)
+            {
+                _reporter.Output($"Launching browser: {fileName} {args}");
+                return;
+            }
+
+            _browserProcess = Process.Start(new ProcessStartInfo
+            {
+                FileName = fileName,
+                Arguments = args,
+                UseShellExecute = true,
+            });
+        }
+
+        private static bool CanLaunchBrowser(DotNetWatchContext context, out string launchUrl)
+        {
+            launchUrl = null;
+            var reporter = context.Reporter;
+
+            if (!context.FileSet.IsNetCoreApp31OrNewer)
+            {
+                // Browser refresh middleware supports 3.1 or newer
+                reporter.Verbose("Browser refresh is only supported in .NET Core 3.1 or newer projects.");
+                return false;
+            }
+
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                // Launching a browser requires file associations that are not available in all operating systems.
+                reporter.Verbose("Browser refresh is only supported in Windows and MacOS.");
+                return false;
+            }
+
+            var dotnetCommand = context.ProcessSpec.Arguments.FirstOrDefault();
+            if (!string.Equals(dotnetCommand, "run", StringComparison.Ordinal))
+            {
+                reporter.Verbose("Browser refresh is only supported for run commands.");
+                return false;
+            }
+
+            // We're executing the run-command. Determine if the launchSettings allows it
+            var launchSettingsPath = Path.Combine(context.ProcessSpec.WorkingDirectory, "Properties", "launchSettings.json");
+            if (!File.Exists(launchSettingsPath))
+            {
+                reporter.Verbose($"No launchSettings.json file found at {launchSettingsPath}. Unable to determine if browser refresh is allowed.");
+                return false;
+            }
+
+            LaunchSettingsJson launchSettings;
+            try
+            {
+                launchSettings = JsonSerializer.Deserialize<LaunchSettingsJson>(
+                    File.ReadAllText(launchSettingsPath),
+                    new JsonSerializerOptions(JsonSerializerDefaults.Web));
+            }
+            catch (Exception ex)
+            {
+                reporter.Verbose($"Error reading launchSettings.json: {ex}.");
+                return false;
+            }
+
+            var defaultProfile = launchSettings.Profiles.FirstOrDefault(f => f.Value.CommandName == "Project").Value;
+            if (defaultProfile is null)
+            {
+                reporter.Verbose("Unable to find default launchSettings profile.");
+                return false;
+            }
+
+            if (!defaultProfile.LaunchBrowser)
+            {
+                reporter.Verbose("launchSettings does not allow launching browsers.");
+                return false;
+            }
+
+            launchUrl = defaultProfile.LaunchUrl;
+            return true;
+        }
+
+        public async ValueTask DisposeAsync()
+        {
+            _browserProcess?.Dispose();
+            if (_refreshServer != null)
+            {
+                await _refreshServer.DisposeAsync();
+            }
+        }
+    }
+}

+ 21 - 0
src/Tools/dotnet-watch/src/LaunchSettingsJson.cs

@@ -0,0 +1,21 @@
+// 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.
+
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Watcher.Tools
+{
+    public class LaunchSettingsJson
+    {
+        public Dictionary<string, LaunchSettingsProfile> Profiles { get; set; }
+    }
+
+    public class LaunchSettingsProfile
+    {
+        public string CommandName { get; set; }
+
+        public bool LaunchBrowser { get; set; }
+
+        public string LaunchUrl { get; set; }
+    }
+}

+ 7 - 1
src/Tools/dotnet-watch/src/ProcessSpec.cs

@@ -1,8 +1,10 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
+using System.Threading;
 using Microsoft.DotNet.Watcher.Internal;
 
 namespace Microsoft.DotNet.Watcher
@@ -19,5 +21,9 @@ namespace Microsoft.DotNet.Watcher
             => Path.GetFileNameWithoutExtension(Executable);
 
         public bool IsOutputCaptured => OutputCapture != null;
+
+        public DataReceivedEventHandler OnOutput { get; set; }
+
+        public CancellationToken CancelOutputCapture { get; set; }
     }
 }

+ 2 - 2
src/Tools/dotnet-watch/src/Program.cs

@@ -162,8 +162,8 @@ namespace Microsoft.DotNet.Watcher
                 _reporter.Output("Polling file watcher is enabled");
             }
 
-            await new DotNetWatcher(reporter, fileSetFactory)
-                .WatchAsync(processInfo, cancellationToken);
+            await using var watcher = new DotNetWatcher(reporter, fileSetFactory);
+            await watcher.WatchAsync(processInfo, cancellationToken);
 
             return 0;
         }

+ 14 - 1
src/Tools/dotnet-watch/src/assets/DotNetWatch.targets

@@ -9,9 +9,22 @@ them to a file.
 -->
   <Target Name="GenerateWatchList"
           DependsOnTargets="_CollectWatchItems">
+
+    <PropertyGroup>
+      <_IsMicrosoftNETCoreApp31OrNewer
+          Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.1'))">true</_IsMicrosoftNETCoreApp31OrNewer>
+
+      <_IsMicrosoftNETCoreApp31OrNewer Condition="'$(_IsMicrosoftNETCoreApp31OrNewer)' == ''">false</_IsMicrosoftNETCoreApp31OrNewer>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_WatchListLine Include="$(_IsMicrosoftNETCoreApp31OrNewer)" />
+      <_WatchListLine Include="%(Watch.FullPath)" />
+    </ItemGroup>
+
     <WriteLinesToFile Overwrite="true"
                       File="$(_DotNetWatchListFile)"
-                      Lines="@(Watch -> '%(FullPath)')" />
+                      Lines="@(_WatchListLine)" />
   </Target>
 
 <!--

+ 20 - 0
src/Tools/dotnet-watch/src/dotnet-watch.csproj

@@ -19,4 +19,24 @@
     <None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.WebSockets" />
+  </ItemGroup>
+
+  <Target Name="_FixupRuntimeConfig" BeforeTargets="_GenerateRuntimeConfigurationFilesInputCache">
+    <ItemGroup>
+      <_RuntimeFramework Include="@(RuntimeFramework)" />
+      <RuntimeFramework Remove="@(RuntimeFramework)" />
+      <RuntimeFramework Include="Microsoft.AspNetCore.App" FrameworkName="Microsoft.AspNetCore.App" Version="5.0.0-preview" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_UndoRuntimeConfigWorkarounds" AfterTargets="GenerateBuildRuntimeConfigurationFiles">
+    <ItemGroup>
+      <RuntimeFramework Remove="@(RuntimeFramework)" />
+      <RuntimeFramework Include="@(_RuntimeFramework)" />
+    </ItemGroup>
+  </Target>
+
 </Project>

+ 3 - 0
src/Tools/dotnet-watch/src/runtimeconfig.template.json

@@ -0,0 +1,3 @@
+{
+  "rollForwardOnNoCandidateFx": 2
+}

+ 68 - 0
src/Tools/dotnet-watch/test/BrowserLaunchTests.cs

@@ -0,0 +1,68 @@
+// 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.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
+{
+    public class BrowserLaunchTests
+    {
+        private readonly WatchableApp _app;
+
+        public BrowserLaunchTests(ITestOutputHelper logger)
+        {
+            _app = new WatchableApp("AppWithLaunchSettings", logger);
+        }
+
+        [ConditionalFact]
+        [OSSkipCondition(OperatingSystems.Linux)]
+        public async Task LaunchesBrowserOnStart()
+        {
+            var expected = "watch : Launching browser: https://localhost:5001/";
+            _app.DotnetWatchArgs.Add("--verbose");
+
+            await _app.StartWatcherAsync();
+
+            // Verify we launched the browser.
+            await _app.Process.GetOutputLineStartsWithAsync(expected, TimeSpan.FromMinutes(2));
+        }
+
+        [ConditionalFact]
+        [OSSkipCondition(OperatingSystems.Linux)]
+        public async Task RefreshesBrowserOnChange()
+        {
+            var launchBrowserMessage = "watch : Launching browser: https://localhost:5001/";
+            var refreshBrowserMessage = "watch : Reloading browser";
+            _app.DotnetWatchArgs.Add("--verbose");
+            var source = Path.Combine(_app.SourceDirectory, "Program.cs");
+
+            await _app.StartWatcherAsync();
+
+            // Verify we launched the browser.
+            await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
+
+            // Make a file change and verify we reloaded the browser.
+            File.SetLastWriteTime(source, DateTime.Now);
+            await _app.Process.GetOutputLineStartsWithAsync(refreshBrowserMessage, TimeSpan.FromMinutes(2));
+        }
+
+        [ConditionalFact]
+        [OSSkipCondition(OperatingSystems.Linux)]
+        public async Task UsesBrowserSpecifiedInEnvironment()
+        {
+            var launchBrowserMessage = "watch : Launching browser: mycustombrowser.bat https://localhost:5001/";
+            _app.EnvironmentVariables.Add("DOTNET_WATCH_BROWSER_PATH", "mycustombrowser.bat");
+
+            _app.DotnetWatchArgs.Add("--verbose");
+
+            await _app.StartWatcherAsync();
+
+            // Verify we launched the browser.
+            await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
+        }
+    }
+}

Some files were not shown because too many files changed in this diff