Skip to content

Commit 8ca580a

Browse files
committed
Add support for binding record types
1 parent 6f37e8a commit 8ca580a

File tree

59 files changed

+14113
-3851
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+14113
-3851
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public interface IPropertyFilterProvider
1212
{
1313
/// <summary>
1414
/// Gets a predicate which can determines which model properties should be bound by model binding.
15+
/// <para>
16+
/// This predicates are also applied to determine which parameters are bound when a model's constructor is bound.
17+
/// </para>
1518
/// </summary>
1619
Func<ModelMetadata, bool> PropertyFilter { get; }
1720
}

src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelBindingMessageProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,31 @@ public abstract class ModelBindingMessageProvider
4343
/// <summary>
4444
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
4545
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
46-
/// with a property.
46+
/// with a property or parameter.
4747
/// </summary>
4848
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid for {1}.".</value>
4949
public virtual Func<string, string, string> AttemptedValueIsInvalidAccessor { get; } = default!;
5050

5151
/// <summary>
5252
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
5353
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
54-
/// with a collection element or action parameter.
54+
/// with a collection element.
5555
/// </summary>
5656
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid.".</value>
5757
public virtual Func<string, string> NonPropertyAttemptedValueIsInvalidAccessor { get; } = default!;
5858

5959
/// <summary>
6060
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
6161
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
62-
/// with a property.
62+
/// with a property or parameter.
6363
/// </summary>
6464
/// <value>Default <see cref="string"/> is "The supplied value is invalid for {0}.".</value>
6565
public virtual Func<string, string> UnknownValueIsInvalidAccessor { get; } = default!;
6666

6767
/// <summary>
6868
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
6969
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
70-
/// with a collection element or action parameter.
70+
/// with a collection element .
7171
/// </summary>
7272
/// <value>Default <see cref="string"/> is "The supplied value is invalid.".</value>
7373
public virtual Func<string> NonPropertyUnknownValueIsInvalidAccessor { get; } = default!;

src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataIdentity.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ private ModelMetadataIdentity(
1616
Type modelType,
1717
string? name = null,
1818
Type? containerType = null,
19-
object? fieldInfo = null)
19+
object? fieldInfo = null,
20+
ConstructorInfo? constructorInfo = null)
2021
{
2122
ModelType = modelType;
2223
Name = name;
2324
ContainerType = containerType;
2425
FieldInfo = fieldInfo;
26+
ConstructorInfo = constructorInfo;
2527
}
2628

2729
/// <summary>
@@ -130,6 +132,28 @@ public static ModelMetadataIdentity ForParameter(ParameterInfo parameter, Type m
130132
return new ModelMetadataIdentity(modelType, parameter.Name, fieldInfo: parameter);
131133
}
132134

135+
/// <summary>
136+
/// Creates a <see cref="ModelMetadataIdentity"/> for the provided parameter with the specified
137+
/// model type.
138+
/// </summary>
139+
/// <param name="constructor">The <see cref="ConstructorInfo" />.</param>
140+
/// <param name="modelType">The model type.</param>
141+
/// <returns>A <see cref="ModelMetadataIdentity"/>.</returns>
142+
public static ModelMetadataIdentity ForConstructor(ConstructorInfo constructor, Type modelType)
143+
{
144+
if (constructor == null)
145+
{
146+
throw new ArgumentNullException(nameof(constructor));
147+
}
148+
149+
if (modelType == null)
150+
{
151+
throw new ArgumentNullException(nameof(modelType));
152+
}
153+
154+
return new ModelMetadataIdentity(modelType, constructor.Name, constructorInfo: constructor);
155+
}
156+
133157
/// <summary>
134158
/// Gets the <see cref="Type"/> defining the model property represented by the current
135159
/// instance, or <c>null</c> if the current instance does not represent a property.
@@ -152,6 +176,10 @@ public ModelMetadataKind MetadataKind
152176
{
153177
return ModelMetadataKind.Parameter;
154178
}
179+
else if (ConstructorInfo != null)
180+
{
181+
return ModelMetadataKind.Constructor;
182+
}
155183
else if (ContainerType != null && Name != null)
156184
{
157185
return ModelMetadataKind.Property;
@@ -183,6 +211,12 @@ public ModelMetadataKind MetadataKind
183211
/// </summary>
184212
public PropertyInfo? PropertyInfo => FieldInfo as PropertyInfo;
185213

214+
/// <summary>
215+
/// Gets a descriptor for the constructor, or <c>null</c> if this instance
216+
/// does not represent a constructor.
217+
/// </summary>
218+
public ConstructorInfo? ConstructorInfo { get; }
219+
186220
/// <inheritdoc />
187221
public bool Equals(ModelMetadataIdentity other)
188222
{
@@ -191,7 +225,8 @@ public bool Equals(ModelMetadataIdentity other)
191225
ModelType == other.ModelType &&
192226
Name == other.Name &&
193227
ParameterInfo == other.ParameterInfo &&
194-
PropertyInfo == other.PropertyInfo;
228+
PropertyInfo == other.PropertyInfo &&
229+
ConstructorInfo == other.ConstructorInfo;
195230
}
196231

197232
/// <inheritdoc />
@@ -210,6 +245,7 @@ public override int GetHashCode()
210245
hash.Add(Name, StringComparer.Ordinal);
211246
hash.Add(ParameterInfo);
212247
hash.Add(PropertyInfo);
248+
hash.Add(ConstructorInfo);
213249
return hash.ToHashCode();
214250
}
215251
}

src/Mvc/Mvc.Abstractions/src/ModelBinding/Metadata/ModelMetadataKind.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,10 @@ public enum ModelMetadataKind
2222
/// Used for <see cref="ModelMetadata"/> for a parameter.
2323
/// </summary>
2424
Parameter,
25+
26+
/// <summary>
27+
/// <see cref="ModelMetadata"/> for a constructor.
28+
/// </summary>
29+
Constructor,
2530
}
26-
}
31+
}

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

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Collections.ObjectModel;
78
using System.ComponentModel;
89
using System.Diagnostics;
10+
using System.Linq;
911
using System.Reflection;
1012
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
1113
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -24,7 +26,11 @@ public abstract class ModelMetadata : IEquatable<ModelMetadata?>, IModelMetadata
2426
/// </summary>
2527
public static readonly int DefaultOrder = 10000;
2628

29+
private static readonly IReadOnlyDictionary<ModelMetadata, ModelMetadata> EmptyParameterMapping = new Dictionary<ModelMetadata, ModelMetadata>(0);
30+
2731
private int? _hashCode;
32+
private IReadOnlyList<ModelMetadata>? _boundProperties;
33+
private IReadOnlyDictionary<ModelMetadata, ModelMetadata>? _parameterMapping;
2834

2935
/// <summary>
3036
/// Creates a new <see cref="ModelMetadata"/>.
@@ -83,7 +89,7 @@ public virtual ModelMetadata ContainerMetadata
8389
/// <summary>
8490
/// Gets the key for the current instance.
8591
/// </summary>
86-
protected ModelMetadataIdentity Identity { get; }
92+
protected internal ModelMetadataIdentity Identity { get; }
8793

8894
/// <summary>
8995
/// Gets a collection of additional information about the model.
@@ -95,6 +101,94 @@ public virtual ModelMetadata ContainerMetadata
95101
/// </summary>
96102
public abstract ModelPropertyCollection Properties { get; }
97103

104+
internal IReadOnlyList<ModelMetadata> BoundProperties
105+
{
106+
get
107+
{
108+
// An item may appear as both a constructor parameter and a property. For instance, in record types,
109+
// each constructor parameter is also a settable property and will have the same name, possibly with a difference in case.
110+
// Executing model binding on these parameters twice may have detrimental effects, such as duplicate validation entries,
111+
// or failures if a model expects to be bound exactly ones.
112+
// Consequently when a bound constructor is present, we only bind and validate the subset of properties whose names
113+
// haven't appeared as parameters.
114+
if (BoundConstructor is null)
115+
{
116+
return Properties;
117+
}
118+
119+
if (_boundProperties is null)
120+
{
121+
var boundParameters = BoundConstructor.Parameters;
122+
var boundProperties = new List<ModelMetadata>();
123+
124+
foreach (var metadata in Properties)
125+
{
126+
if (!boundParameters.Any(p =>
127+
string.Equals(p.ParameterName, metadata.PropertyName, StringComparison.OrdinalIgnoreCase)
128+
&& p.ModelType == metadata.ModelType))
129+
{
130+
boundProperties.Add(metadata);
131+
}
132+
}
133+
134+
_boundProperties = boundProperties;
135+
}
136+
137+
return _boundProperties;
138+
}
139+
}
140+
141+
internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> ParameterMapping
142+
{
143+
get
144+
{
145+
if (_parameterMapping != null)
146+
{
147+
return _parameterMapping;
148+
}
149+
150+
if (BoundConstructor is null)
151+
{
152+
_parameterMapping = EmptyParameterMapping;
153+
return _parameterMapping;
154+
}
155+
156+
var boundParameters = BoundConstructor.Parameters;
157+
var parameterMapping = new Dictionary<ModelMetadata, ModelMetadata>();
158+
159+
foreach (var parameter in boundParameters)
160+
{
161+
var property = Properties.FirstOrDefault(p =>
162+
string.Equals(p.Name, parameter.ParameterName, StringComparison.OrdinalIgnoreCase) &&
163+
p.ModelType == parameter.ModelType);
164+
165+
if (property != null)
166+
{
167+
parameterMapping[parameter] = property;
168+
}
169+
}
170+
171+
_parameterMapping = parameterMapping;
172+
return _parameterMapping;
173+
}
174+
}
175+
176+
/// <summary>
177+
/// 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>
183+
/// </summary>
184+
public virtual ModelMetadata? BoundConstructor { get; }
185+
186+
/// <summary>
187+
/// Gets the collection of <see cref="ModelMetadata"/> instances for a constructor's parameters.
188+
/// This is only available when <see cref="MetadataKind"/> is <see cref="ModelMetadataKind.Constructor"/>.
189+
/// </summary>
190+
public abstract IReadOnlyList<ModelMetadata> Parameters { get; }
191+
98192
/// <summary>
99193
/// Gets the name of a model if specified explicitly using <see cref="IModelNameProvider"/>.
100194
/// </summary>
@@ -401,6 +495,11 @@ public virtual ModelMetadata ContainerMetadata
401495
/// </summary>
402496
public abstract Action<object, object> PropertySetter { get; }
403497

498+
/// <summary>
499+
/// Gets a delegate that invokes the constructor.
500+
/// </summary>
501+
public abstract Func<object[], object> ConstructorInvoker { get; }
502+
404503
/// <summary>
405504
/// Gets a display name for the model.
406505
/// </summary>
@@ -500,6 +599,8 @@ private string DebuggerToString()
500599
return $"ModelMetadata (Property: '{ContainerType!.Name}.{PropertyName}' Type: '{ModelType.Name}')";
501600
case ModelMetadataKind.Type:
502601
return $"ModelMetadata (Type: '{ModelType.Name}')";
602+
case ModelMetadataKind.Constructor:
603+
return $"ModelMetadata (Constructor: '{ModelType.Name}')";
503604
default:
504605
return $"Unsupported MetadataKind '{MetadataKind}'.";
505606
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,16 @@ public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, T
5454
{
5555
throw new NotSupportedException();
5656
}
57+
58+
/// <summary>
59+
/// Supplies metadata describing a property.
60+
/// </summary>
61+
/// <param name="constructor">The <see cref="ConstructorInfo"/>.</param>
62+
/// <param name="modelType">The type declaring the constructor.</param>
63+
/// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="constructor"/>.</returns>
64+
public virtual ModelMetadata GetMetadataForConstructor(ConstructorInfo constructor, Type modelType)
65+
{
66+
throw new NotSupportedException();
67+
}
5768
}
5869
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.CompilerServices;
1010
using Microsoft.AspNetCore.Mvc.Abstractions;
1111
using Microsoft.AspNetCore.Mvc.Formatters;
12+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
1213
using Microsoft.Extensions.Primitives;
1314

1415
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -298,7 +299,9 @@ public bool TryAddModelError(string key, Exception exception, ModelMetadata meta
298299
// "The value '' is not valid." (when no value was provided, not even an empty string) and
299300
// "The supplied value is invalid for Int32." (when error is for an element or parameter).
300301
var messageProvider = metadata.ModelBindingMessageProvider;
301-
var name = metadata.DisplayName ?? metadata.PropertyName;
302+
303+
var name = metadata.DisplayName ??
304+
((metadata.MetadataKind == ModelMetadataKind.Parameter) ? metadata.ParameterName : metadata.PropertyName);
302305
string errorMessage;
303306
if (entry == null && name == null)
304307
{

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesValuesFromBindingI
8383
// Arrange
8484
var attributes = new object[]
8585
{
86-
new ModelBinderAttribute { BinderType = typeof(ComplexTypeModelBinder), Name = "Test" },
86+
new ModelBinderAttribute { BinderType = typeof(ComplexObjectModelBinder), Name = "Test" },
8787
};
8888
var modelType = typeof(Guid);
8989
var provider = new TestModelMetadataProvider();
@@ -100,7 +100,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesValuesFromBindingI
100100

101101
// Assert
102102
Assert.NotNull(bindingInfo);
103-
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
103+
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
104104
Assert.Same("Test", bindingInfo.BinderModelName);
105105
}
106106

@@ -110,7 +110,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesBinderNameFromMode
110110
// Arrange
111111
var attributes = new object[]
112112
{
113-
new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
113+
new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
114114
new ControllerAttribute(),
115115
new BindNeverAttribute(),
116116
};
@@ -129,7 +129,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesBinderNameFromMode
129129

130130
// Assert
131131
Assert.NotNull(bindingInfo);
132-
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
132+
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
133133
Assert.Same("Different", bindingInfo.BinderModelName);
134134
Assert.Same(BindingSource.Custom, bindingInfo.BindingSource);
135135
}
@@ -143,7 +143,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesModelBinderFromMod
143143
var provider = new TestModelMetadataProvider();
144144
provider.ForType(modelType).BindingDetails(metadata =>
145145
{
146-
metadata.BinderType = typeof(ComplexTypeModelBinder);
146+
metadata.BinderType = typeof(ComplexObjectModelBinder);
147147
});
148148
var modelMetadata = provider.GetMetadataForType(modelType);
149149

@@ -152,7 +152,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesModelBinderFromMod
152152

153153
// Assert
154154
Assert.NotNull(bindingInfo);
155-
Assert.Same(typeof(ComplexTypeModelBinder), bindingInfo.BinderType);
155+
Assert.Same(typeof(ComplexObjectModelBinder), bindingInfo.BinderType);
156156
}
157157

158158
[Fact]
@@ -187,7 +187,7 @@ public void GetBindingInfo_WithAttributesAndModelMetadata_UsesPropertyPredicateP
187187
// Arrange
188188
var attributes = new object[]
189189
{
190-
new ModelBinderAttribute(typeof(ComplexTypeModelBinder)),
190+
new ModelBinderAttribute(typeof(ComplexObjectModelBinder)),
191191
new ControllerAttribute(),
192192
new BindNeverAttribute(),
193193
};

0 commit comments

Comments
 (0)