Skip to content

Commit 6721924

Browse files
committed
Add support for binding record types
1 parent 30ded54 commit 6721924

File tree

61 files changed

+14144
-3852
lines changed

Some content is hidden

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

61 files changed

+14144
-3852
lines changed

src/Mvc/Mvc.Abstractions/ref/Microsoft.AspNetCore.Mvc.Abstractions.netcoreapp.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ protected ModelMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMeta
641641
public abstract string BinderModelName { get; }
642642
public abstract System.Type BinderType { get; }
643643
public abstract Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { get; }
644+
public virtual Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata? BoundConstructor { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
645+
public abstract System.Func<object[], object> ConstructorInvoker { get; }
644646
public virtual Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata ContainerMetadata { get { throw null; } }
645647
public System.Type? ContainerType { get { throw null; } }
646648
public abstract bool ConvertEmptyStringToNull { get; }
@@ -657,7 +659,7 @@ protected ModelMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMeta
657659
public virtual bool? HasValidators { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
658660
public abstract bool HideSurroundingHtml { get; }
659661
public abstract bool HtmlEncode { get; }
660-
protected Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity Identity { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
662+
protected internal Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity Identity { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
661663
public abstract bool IsBindingAllowed { get; }
662664
public abstract bool IsBindingRequired { get; }
663665
public bool IsCollectionType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
@@ -676,6 +678,7 @@ protected ModelMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMeta
676678
public abstract string NullDisplayText { get; }
677679
public abstract int Order { get; }
678680
public string? ParameterName { get { throw null; } }
681+
public abstract System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata> Parameters { get; }
679682
public abstract string Placeholder { get; }
680683
public abstract Microsoft.AspNetCore.Mvc.ModelBinding.ModelPropertyCollection Properties { get; }
681684
public abstract Microsoft.AspNetCore.Mvc.ModelBinding.IPropertyFilterProvider PropertyFilterProvider { get; }
@@ -700,6 +703,7 @@ protected ModelMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMeta
700703
public abstract partial class ModelMetadataProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider
701704
{
702705
protected ModelMetadataProvider() { }
706+
public virtual Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata GetMetadataForConstructor(System.Reflection.ConstructorInfo constructor, System.Type modelType) { throw null; }
703707
public abstract Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata GetMetadataForParameter(System.Reflection.ParameterInfo parameter);
704708
public virtual Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata GetMetadataForParameter(System.Reflection.ParameterInfo parameter, System.Type modelType) { throw null; }
705709
public abstract System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata> GetMetadataForProperties(System.Type modelType);
@@ -897,6 +901,7 @@ protected ModelBindingMessageProvider() { }
897901
{
898902
private readonly object _dummy;
899903
private readonly int _dummyPrimitive;
904+
public System.Reflection.ConstructorInfo? ConstructorInfo { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
900905
public System.Type? ContainerType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
901906
public Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataKind MetadataKind { get { throw null; } }
902907
public System.Type ModelType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
@@ -905,6 +910,7 @@ protected ModelBindingMessageProvider() { }
905910
public System.Reflection.PropertyInfo? PropertyInfo { get { throw null; } }
906911
public bool Equals(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity other) { throw null; }
907912
public override bool Equals(object? obj) { throw null; }
913+
public static Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity ForConstructor(System.Reflection.ConstructorInfo constructor, System.Type modelType) { throw null; }
908914
public static Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity ForParameter(System.Reflection.ParameterInfo parameter) { throw null; }
909915
public static Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity ForParameter(System.Reflection.ParameterInfo parameter, System.Type modelType) { throw null; }
910916
public static Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity ForProperty(System.Reflection.PropertyInfo propertyInfo, System.Type modelType, System.Type containerType) { throw null; }
@@ -918,6 +924,7 @@ public enum ModelMetadataKind
918924
Type = 0,
919925
Property = 1,
920926
Parameter = 2,
927+
Constructor = 3,
921928
}
922929
}
923930
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation

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
{

0 commit comments

Comments
 (0)