Skip to content

Commit 6e6269a

Browse files
committed
PR changes
1 parent b9ab7de commit 6e6269a

File tree

12 files changed

+108
-106
lines changed

12 files changed

+108
-106
lines changed

src/Mvc/Mvc.Abstractions/src/ModelBinding/IPropertyFilterProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public interface IPropertyFilterProvider
1313
/// <summary>
1414
/// Gets a predicate which can determines which model properties should be bound by model binding.
1515
/// <para>
16-
/// This predicates are also applied to determine which parameters are bound when a model's constructor is bound.
16+
/// This predicate is also used to determine which parameters are bound when a model's constructor is bound.
1717
/// </para>
1818
/// </summary>
1919
Func<ModelMetadata, bool> PropertyFilter { get; }

src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,13 @@ internal IReadOnlyList<ModelMetadata> BoundProperties
118118

119119
if (_boundProperties is null)
120120
{
121-
var boundParameters = BoundConstructor.Parameters;
121+
var boundParameters = BoundConstructor.Parameters!;
122122
var boundProperties = new List<ModelMetadata>();
123123

124124
foreach (var metadata in Properties)
125125
{
126126
if (!boundParameters.Any(p =>
127-
string.Equals(p.ParameterName, metadata.PropertyName, StringComparison.OrdinalIgnoreCase)
127+
string.Equals(p.ParameterName, metadata.PropertyName, StringComparison.Ordinal)
128128
&& p.ModelType == metadata.ModelType))
129129
{
130130
boundProperties.Add(metadata);
@@ -153,13 +153,13 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> ParameterMapping
153153
return _parameterMapping;
154154
}
155155

156-
var boundParameters = BoundConstructor.Parameters;
156+
var boundParameters = BoundConstructor.Parameters!;
157157
var parameterMapping = new Dictionary<ModelMetadata, ModelMetadata>();
158158

159159
foreach (var parameter in boundParameters)
160160
{
161161
var property = Properties.FirstOrDefault(p =>
162-
string.Equals(p.Name, parameter.ParameterName, StringComparison.OrdinalIgnoreCase) &&
162+
string.Equals(p.Name, parameter.ParameterName, StringComparison.Ordinal) &&
163163
p.ModelType == parameter.ModelType);
164164

165165
if (property != null)
@@ -175,19 +175,14 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> ParameterMapping
175175

176176
/// <summary>
177177
/// Gets <see cref="ModelMetadata"/> instance for a constructor that is used during binding and validation.
178-
/// <para>
179-
/// A constructor is used during model binding and validation if it is the only constructor on the type,
180-
/// is a parameterless constructor on a type with multiple constructors, or is a constructor with the
181-
/// <c>ModelBindingConstructorAttribute</c>.
182-
/// </para>
183178
/// </summary>
184179
public virtual ModelMetadata? BoundConstructor { get; }
185180

186181
/// <summary>
187182
/// Gets the collection of <see cref="ModelMetadata"/> instances for a constructor's parameters.
188183
/// This is only available when <see cref="MetadataKind"/> is <see cref="ModelMetadataKind.Constructor"/>.
189184
/// </summary>
190-
public abstract IReadOnlyList<ModelMetadata> Parameters { get; }
185+
public virtual IReadOnlyList<ModelMetadata>? Parameters { get; }
191186

192187
/// <summary>
193188
/// Gets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.

src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadataProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -56,7 +56,7 @@ public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, T
5656
}
5757

5858
/// <summary>
59-
/// Supplies metadata describing a property.
59+
/// Supplies metadata describing a constructor.
6060
/// </summary>
6161
/// <param name="constructor">The <see cref="ConstructorInfo"/>.</param>
6262
/// <param name="modelType">The type declaring the constructor.</param>

src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,7 @@ public bool TryAddModelError(string key, Exception exception, ModelMetadata meta
300300
// "The supplied value is invalid for Int32." (when error is for an element or parameter).
301301
var messageProvider = metadata.ModelBindingMessageProvider;
302302

303-
var name = metadata.DisplayName ??
304-
((metadata.MetadataKind == ModelMetadataKind.Parameter) ? metadata.ParameterName : metadata.PropertyName);
303+
var name = metadata.DisplayName ?? metadata.PropertyName;
305304
string errorMessage;
306305
if (entry == null && name == null)
307306
{

src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,8 +1025,8 @@ public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_Wit
10251025
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
10261026
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
10271027
var optionsAccessor = new OptionsAccessor();
1028-
optionsAccessor.Value.ModelBindingMessageProvider.SetUnknownValueIsInvalidAccessor(
1029-
_ => "Hmm, the supplied value is not valid.");
1028+
optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(
1029+
() => "Hmm, the supplied value is not valid.");
10301030

10311031
var method = typeof(string).GetMethod(nameof(string.Copy));
10321032
var parameter = method.GetParameters()[0]; // Copy(string str)
@@ -1141,8 +1141,8 @@ public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithPa
11411141
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
11421142
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
11431143
var optionsAccessor = new OptionsAccessor();
1144-
optionsAccessor.Value.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor(
1145-
(value, _) => $"Hmm, the value '{ value }' is not valid.");
1144+
optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
1145+
value => $"Hmm, the value '{ value }' is not valid.");
11461146

11471147
var method = typeof(string).GetMethod(nameof(string.Copy));
11481148
var parameter = method.GetParameters()[0]; // Copy(string str)

src/Mvc/Mvc.Core/src/ModelBinding/Binders/ComplexObjectModelBinder.cs

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Buffers;
65
using System.Collections.Generic;
76
using System.Diagnostics;
7+
using System.Linq.Expressions;
88
using System.Reflection;
99
using System.Threading.Tasks;
1010
using Microsoft.AspNetCore.Mvc.Core;
11-
using Microsoft.AspNetCore.Mvc.Infrastructure;
1211
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
1312
using Microsoft.Extensions.Logging;
1413

@@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
1716
/// <summary>
1817
/// <see cref="IModelBinder"/> implementation for binding complex types.
1918
/// </summary>
20-
public class ComplexObjectModelBinder : IModelBinder
19+
public sealed class ComplexObjectModelBinder : IModelBinder
2120
{
2221
// Don't want a new public enum because communication between the private and internal methods of this class
2322
// should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.
@@ -35,6 +34,8 @@ public class ComplexObjectModelBinder : IModelBinder
3534
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
3635
private readonly IReadOnlyList<IModelBinder> _parameterBinders;
3736
private readonly ILogger _logger;
37+
private Func<object> _modelCreator;
38+
3839

3940
internal ComplexObjectModelBinder(
4041
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
@@ -79,15 +80,9 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int pr
7980
if (bindingContext.Model == null)
8081
{
8182
var boundConstructor = modelMetadata.BoundConstructor;
82-
if (boundConstructor is null)
83-
{
84-
ThrowObjectCannotBeCreated(modelMetadata);
85-
}
86-
87-
object[] values;
88-
if (boundConstructor.Parameters.Count > 0)
83+
if (boundConstructor != null)
8984
{
90-
values = new object[boundConstructor.Parameters.Count];
85+
var values = new object[boundConstructor.Parameters.Count];
9186
var (attemptedParameterBinding, parameterBindingSucceeded) = await BindParameters(
9287
bindingContext,
9388
propertyData,
@@ -96,15 +91,15 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int pr
9691

9792
attemptedBinding |= attemptedParameterBinding;
9893
bindingSucceeded |= parameterBindingSucceeded;
94+
95+
if (!CreateModel(bindingContext, boundConstructor, values))
96+
{
97+
return;
98+
}
9999
}
100100
else
101101
{
102-
values = Array.Empty<object>();
103-
}
104-
105-
if (!CreateModel(bindingContext, boundConstructor, values))
106-
{
107-
return;
102+
CreateModel(bindingContext);
108103
}
109104
}
110105

@@ -178,6 +173,58 @@ internal static bool CreateModel(ModelBindingContext bindingContext, ModelMetada
178173
}
179174
}
180175

176+
/// <summary>
177+
/// Creates suitable <see cref="object"/> for given <paramref name="bindingContext"/>.
178+
/// </summary>
179+
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
180+
/// <returns>An <see cref="object"/> compatible with <see cref="ModelBindingContext.ModelType"/>.</returns>
181+
internal void CreateModel(ModelBindingContext bindingContext)
182+
{
183+
if (bindingContext == null)
184+
{
185+
throw new ArgumentNullException(nameof(bindingContext));
186+
}
187+
188+
// If model creator throws an exception, we want to propagate it back up the call stack, since the
189+
// application developer should know that this was an invalid type to try to bind to.
190+
if (_modelCreator == null)
191+
{
192+
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
193+
// reflection does not provide information about the implicit parameterless constructor for a struct.
194+
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
195+
// compile fails to construct it.
196+
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
197+
if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
198+
{
199+
var metadata = bindingContext.ModelMetadata;
200+
switch (metadata.MetadataKind)
201+
{
202+
case ModelMetadataKind.Parameter:
203+
throw new InvalidOperationException(
204+
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
205+
modelTypeInfo.FullName,
206+
metadata.ParameterName));
207+
case ModelMetadataKind.Property:
208+
throw new InvalidOperationException(
209+
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
210+
modelTypeInfo.FullName,
211+
metadata.PropertyName,
212+
bindingContext.ModelMetadata.ContainerType.FullName));
213+
case ModelMetadataKind.Type:
214+
throw new InvalidOperationException(
215+
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
216+
modelTypeInfo.FullName));
217+
}
218+
}
219+
220+
_modelCreator = Expression
221+
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
222+
.Compile();
223+
}
224+
225+
bindingContext.Model = _modelCreator();
226+
}
227+
181228
private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindParameters(
182229
ModelBindingContext bindingContext,
183230
int propertyData,
@@ -671,32 +718,6 @@ internal void SetProperty(
671718
}
672719
}
673720

674-
private void ThrowObjectCannotBeCreated(ModelMetadata metadata)
675-
{
676-
var modelType = metadata.ModelType;
677-
switch (metadata.MetadataKind)
678-
{
679-
case ModelMetadataKind.Parameter:
680-
throw new InvalidOperationException(
681-
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
682-
modelType.FullName,
683-
metadata.ParameterName));
684-
case ModelMetadataKind.Property:
685-
throw new InvalidOperationException(
686-
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
687-
modelType.FullName,
688-
metadata.PropertyName,
689-
metadata.ContainerType.FullName));
690-
case ModelMetadataKind.Type:
691-
throw new InvalidOperationException(
692-
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
693-
modelType.FullName));
694-
case ModelMetadataKind.Constructor:
695-
Debug.Fail("We do not expect constructors here.");
696-
break;
697-
}
698-
}
699-
700721
private static void AddModelError(
701722
Exception exception,
702723
string modelName,

src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultBindingMetadataProvider.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ internal static ConstructorInfo GetBoundConstructor(Type type)
9393
return null;
9494
}
9595

96-
var parameterlessConstructor = constructors.FirstOrDefault(f => f.GetParameters().Length == 0);
97-
return GetRecordTypeConstructor(type, constructors) ?? parameterlessConstructor;
96+
return GetRecordTypeConstructor(type, constructors);
9897
}
9998

10099
private static ConstructorInfo GetRecordTypeConstructor(Type type, ConstructorInfo[] constructors)
@@ -121,21 +120,15 @@ private static ConstructorInfo GetRecordTypeConstructor(Type type, ConstructorIn
121120
for (var i = 0; i < parameters.Length; i++)
122121
{
123122
var parameter = parameters[i];
124-
var mappedProperties = properties.Where(property =>
125-
string.Equals(property.Name, parameter.Name, StringComparison.OrdinalIgnoreCase) &&
126-
property.Property.PropertyType == parameter.ParameterType)
127-
.ToList();
123+
var mappedProperty = properties.FirstOrDefault(property =>
124+
string.Equals(property.Name, parameter.Name, StringComparison.Ordinal) &&
125+
property.Property.PropertyType == parameter.ParameterType);
128126

129-
if (mappedProperties.Count == 0)
127+
if (mappedProperty is null)
130128
{
131129
// No property found, this is not a primary constructor.
132130
return null;
133131
}
134-
else if (mappedProperties.Count > 1)
135-
{
136-
// More than one property found that maps to a constructor parameter. This is ambigious.
137-
return null;
138-
}
139132
}
140133

141134
return constructor;

src/Mvc/Mvc.Core/src/Resources.resx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,13 +523,13 @@
523523
<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>
524524
</data>
525525
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForParameter" xml:space="preserve">
526-
<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 constructor. Alternatively, give the '{1}' parameter a non-null default value.</value>
526+
<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>
527527
</data>
528528
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForProperty" xml:space="preserve">
529-
<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 constructor. Alternatively, set the '{1}' property to a non-null value in the '{2}' constructor.</value>
529+
<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>
530530
</data>
531531
<data name="ComplexObjectModelBinder_NoSuitableConstructor_ForType" xml:space="preserve">
532-
<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 constructor.</value>
532+
<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>
533533
</data>
534534
<data name="ValidationStrategy_MappedPropertyNotFound" xml:space="preserve">
535535
<value>No property found that maps to constructor parameter '{0}' for type '{1}'. Validation requires that each bound parameter must have a property to read the value.</value>

0 commit comments

Comments
 (0)