-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
ComplexObjectModelBinder.cs
751 lines (656 loc) · 31 KB
/
ComplexObjectModelBinder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
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 partial 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;
}
/// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
ArgumentNullException.ThrowIfNull(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;
var boundConstructor = modelMetadata.BoundConstructor;
if (boundConstructor != null)
{
// Only record types are allowed to have a BoundConstructor. Binding a record type requires
// instantiating the type. This means we'll ignore a previously assigned bindingContext.Model value.
// This behaior is identical to input formatting with S.T.Json and Json.NET.
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 if (bindingContext.Model == null)
{
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)
{
ArgumentNullException.ThrowIfNull(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 modelType = bindingContext.ModelType;
if (modelType.IsAbstract || modelType.GetConstructor(Type.EmptyTypes) == null)
{
var metadata = bindingContext.ModelMetadata;
switch (metadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
modelType.FullName,
metadata.ParameterName));
case ModelMetadataKind.Property:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
modelType.FullName,
metadata.PropertyName,
bindingContext.ModelMetadata.ContainerType!.FullName));
case ModelMetadataKind.Type:
throw new InvalidOperationException(
Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
modelType.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 static 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 static 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 static 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;
}
Log.CannotBindToComplexType(_logger, 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.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 static 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 partial class Log
{
[LoggerMessage(17, LogLevel.Debug, "Could not bind to model with name '{ModelName}' and type '{ModelType}' as the type has no " +
"public settable properties or constructor parameters.", EventName = "NoPublicSettableItems")]
public static partial void NoPublicSettableItems(ILogger logger, string modelName, Type modelType);
public static void NoPublicSettableItems(ILogger logger, ModelBindingContext bindingContext)
{
NoPublicSettableItems(logger, bindingContext.ModelName, bindingContext.ModelType);
}
public static void CannotBindToComplexType(ILogger logger, ModelBindingContext bindingContext)
=> CannotBindToComplexType(logger, bindingContext.ModelType);
[LoggerMessage(18, LogLevel.Debug, "Could not bind to model of type '{ModelType}' as there were no values in the request for any of the properties.", EventName = "CannotBindToComplexType")]
private static partial void CannotBindToComplexType(ILogger logger, Type modelType);
}
}