@@ -40,18 +40,22 @@ public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBind
40
40
return null ;
41
41
}
42
42
43
- EnsureModel ( bindingContext ) ;
44
- var result = await CreateAndPopulateDto ( bindingContext , mutableObjectBinderContext . PropertyMetadata ) ;
43
+ // Create model first (if necessary) to avoid reporting errors about properties when activation fails.
44
+ var model = GetModel ( bindingContext ) ;
45
+
46
+ var results = await BindPropertiesAsync ( bindingContext , mutableObjectBinderContext . PropertyMetadata ) ;
45
47
46
48
var validationNode = new ModelValidationNode (
47
49
bindingContext . ModelName ,
48
50
bindingContext . ModelMetadata ,
49
- bindingContext . Model ) ;
51
+ model ) ;
52
+
53
+ // Post-processing e.g. property setters and hooking up validation.
54
+ bindingContext . Model = model ;
55
+ ProcessResults ( bindingContext , results , validationNode ) ;
50
56
51
- // post-processing, e.g. property setters and hooking up validation
52
- ProcessDto ( bindingContext , ( ComplexModelDto ) result . Model , validationNode ) ;
53
57
return new ModelBindingResult (
54
- bindingContext . Model ,
58
+ model ,
55
59
bindingContext . ModelName ,
56
60
isModelSet : true ,
57
61
validationNode : validationNode ) ;
@@ -192,7 +196,8 @@ private async Task<bool> CanBindValue(ModelBindingContext bindingContext)
192
196
var bindingSource = bindingContext . BindingSource ;
193
197
if ( bindingSource != null && ! bindingSource . IsGreedy )
194
198
{
195
- var rootValueProvider = bindingContext . OperationBindingContext . ValueProvider as IBindingSourceValueProvider ;
199
+ var rootValueProvider =
200
+ bindingContext . OperationBindingContext . ValueProvider as IBindingSourceValueProvider ;
196
201
if ( rootValueProvider != null )
197
202
{
198
203
valueProvider = rootValueProvider . Filter ( bindingSource ) ;
@@ -225,12 +230,6 @@ private static bool CanBindType(ModelMetadata modelMetadata)
225
230
return false ;
226
231
}
227
232
228
- if ( modelMetadata . ModelType == typeof ( ComplexModelDto ) )
229
- {
230
- // forbidden type - will cause a stack overflow if we try binding this type
231
- return false ;
232
- }
233
-
234
233
return true ;
235
234
}
236
235
@@ -265,24 +264,50 @@ private static bool CanUpdateReadOnlyProperty(Type propertyType)
265
264
return true ;
266
265
}
267
266
268
- private async Task < ModelBindingResult > CreateAndPopulateDto (
267
+ // Returned dictionary contains entries corresponding to properties against which binding was attempted. If
268
+ // binding failed, the entry's value will have IsModelSet == false. Binding is attempted for all elements of
269
+ // propertyMetadatas.
270
+ private async Task < IDictionary < ModelMetadata , ModelBindingResult > > BindPropertiesAsync (
269
271
ModelBindingContext bindingContext ,
270
272
IEnumerable < ModelMetadata > propertyMetadatas )
271
273
{
272
- // create a DTO and call into the DTO binder
273
- var dto = new ComplexModelDto ( bindingContext . ModelMetadata , propertyMetadatas ) ;
274
-
275
- var metadataProvider = bindingContext . OperationBindingContext . MetadataProvider ;
276
- var dtoMetadata = metadataProvider . GetMetadataForType ( typeof ( ComplexModelDto ) ) ;
274
+ var results = new Dictionary < ModelMetadata , ModelBindingResult > ( ) ;
275
+ foreach ( var propertyMetadata in propertyMetadatas )
276
+ {
277
+ var propertyModelName = ModelNames . CreatePropertyModelName (
278
+ bindingContext . ModelName ,
279
+ propertyMetadata . BinderModelName ?? propertyMetadata . PropertyName ) ;
280
+ var childContext = ModelBindingContext . GetChildModelBindingContext (
281
+ bindingContext ,
282
+ propertyModelName ,
283
+ propertyMetadata ) ;
284
+
285
+ // ModelBindingContext.Model property values may be non-null when invoked via TryUpdateModel(). Pass
286
+ // complex (including collection) values down so that binding system does not unnecessarily recreate
287
+ // instances or overwrite inner properties that are not bound. No need for this with simple values
288
+ // because they will be overwritten if binding succeeds. Arrays are never reused because they cannot
289
+ // be resized.
290
+ //
291
+ // ModelMetadata.PropertyGetter is not null safe; use it only if Model is non-null.
292
+ if ( bindingContext . Model != null &&
293
+ propertyMetadata . PropertyGetter != null &&
294
+ propertyMetadata . IsComplexType &&
295
+ ! propertyMetadata . ModelType . IsArray )
296
+ {
297
+ childContext . Model = propertyMetadata . PropertyGetter ( bindingContext . Model ) ;
298
+ }
277
299
278
- var childContext = ModelBindingContext . GetChildModelBindingContext (
279
- bindingContext ,
280
- bindingContext . ModelName ,
281
- dtoMetadata ) ;
300
+ var result = await bindingContext . OperationBindingContext . ModelBinder . BindModelAsync ( childContext ) ;
301
+ if ( result == null )
302
+ {
303
+ // Could not bind. Let ProcessResult() know explicitly.
304
+ result = new ModelBindingResult ( model : null , key : propertyModelName , isModelSet : false ) ;
305
+ }
282
306
283
- childContext . Model = dto ;
307
+ results [ propertyMetadata ] = result ;
308
+ }
284
309
285
- return await bindingContext . OperationBindingContext . ModelBinder . BindModelAsync ( childContext ) ;
310
+ return results ;
286
311
}
287
312
288
313
/// <summary>
@@ -298,16 +323,18 @@ protected virtual object CreateModel([NotNull] ModelBindingContext bindingContex
298
323
}
299
324
300
325
/// <summary>
301
- /// Ensures <see cref="ModelBindingContext.Model"/> is not <c>null</c> in given
302
- /// <paramref name="bindingContext "/>.
326
+ /// Get <see cref="ModelBindingContext.Model"/> if that property is not <c>null</c>. Otherwise activate a
327
+ /// new instance of <see cref="ModelBindingContext.ModelType "/>.
303
328
/// </summary>
304
329
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
305
- protected virtual void EnsureModel ( [ NotNull ] ModelBindingContext bindingContext )
330
+ protected virtual object GetModel ( [ NotNull ] ModelBindingContext bindingContext )
306
331
{
307
- if ( bindingContext . Model = = null )
332
+ if ( bindingContext . Model ! = null )
308
333
{
309
- bindingContext . Model = CreateModel ( bindingContext ) ;
334
+ return bindingContext . Model ;
310
335
}
336
+
337
+ return CreateModel ( bindingContext ) ;
311
338
}
312
339
313
340
/// <summary>
@@ -365,17 +392,18 @@ internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingCon
365
392
}
366
393
367
394
// Internal for testing.
368
- internal ModelValidationNode ProcessDto (
395
+ internal ModelValidationNode ProcessResults (
369
396
ModelBindingContext bindingContext ,
370
- ComplexModelDto dto ,
397
+ IDictionary < ModelMetadata , ModelBindingResult > results ,
371
398
ModelValidationNode validationNode )
372
399
{
373
400
var metadataProvider = bindingContext . OperationBindingContext . MetadataProvider ;
374
- var modelExplorer = metadataProvider . GetModelExplorerForType ( bindingContext . ModelType , bindingContext . Model ) ;
401
+ var modelExplorer =
402
+ metadataProvider . GetModelExplorerForType ( bindingContext . ModelType , bindingContext . Model ) ;
375
403
var validationInfo = GetPropertyValidationInfo ( bindingContext ) ;
376
404
377
405
// Eliminate provided properties from RequiredProperties; leaving just *missing* required properties.
378
- var boundProperties = dto . Results . Where ( p => p . Value . IsModelSet ) . Select ( p => p . Key . PropertyName ) ;
406
+ var boundProperties = results . Where ( p => p . Value . IsModelSet ) . Select ( p => p . Key . PropertyName ) ;
379
407
validationInfo . RequiredProperties . ExceptWith ( boundProperties ) ;
380
408
381
409
foreach ( var missingRequiredProperty in validationInfo . RequiredProperties )
@@ -389,25 +417,25 @@ internal ModelValidationNode ProcessDto(
389
417
Resources . FormatModelBinding_MissingBindRequiredMember ( propertyName ) ) ;
390
418
}
391
419
392
- // For each property that ComplexModelDtoModelBinder attempted to bind, call the setter, recording
420
+ // For each property that BindPropertiesAsync() attempted to bind, call the setter, recording
393
421
// exceptions as necessary.
394
- foreach ( var entry in dto . Results )
422
+ foreach ( var entry in results )
395
423
{
396
- var dtoResult = entry . Value ;
397
- if ( dtoResult != null )
424
+ var result = entry . Value ;
425
+ if ( result != null )
398
426
{
399
427
var propertyMetadata = entry . Key ;
400
- SetProperty ( bindingContext , modelExplorer , propertyMetadata , dtoResult ) ;
428
+ SetProperty ( bindingContext , modelExplorer , propertyMetadata , result ) ;
401
429
402
- var dtoValidationNode = dtoResult . ValidationNode ;
403
- if ( dtoValidationNode == null )
430
+ var propertyValidationNode = result . ValidationNode ;
431
+ if ( propertyValidationNode == null )
404
432
{
405
- // Make sure that irrespective of if the properties of the model were bound with a value,
433
+ // Make sure that irrespective of whether the properties of the model were bound with a value,
406
434
// create a validation node so that these get validated.
407
- dtoValidationNode = new ModelValidationNode ( dtoResult . Key , entry . Key , dtoResult . Model ) ;
435
+ propertyValidationNode = new ModelValidationNode ( result . Key , entry . Key , result . Model ) ;
408
436
}
409
437
410
- validationNode . ChildNodes . Add ( dtoValidationNode ) ;
438
+ validationNode . ChildNodes . Add ( propertyValidationNode ) ;
411
439
}
412
440
}
413
441
@@ -422,71 +450,69 @@ internal ModelValidationNode ProcessDto(
422
450
/// The <see cref="ModelExplorer"/> for the model containing property to set.
423
451
/// </param>
424
452
/// <param name="propertyMetadata">The <see cref="ModelMetadata"/> for the property to set.</param>
425
- /// <param name="dtoResult ">The <see cref="ModelBindingResult"/> for the property's new value.</param>
453
+ /// <param name="result ">The <see cref="ModelBindingResult"/> for the property's new value.</param>
426
454
/// <remarks>Should succeed in all cases that <see cref="CanUpdateProperty"/> returns <c>true</c>.</remarks>
427
455
protected virtual void SetProperty (
428
456
[ NotNull ] ModelBindingContext bindingContext ,
429
457
[ NotNull ] ModelExplorer modelExplorer ,
430
458
[ NotNull ] ModelMetadata propertyMetadata ,
431
- [ NotNull ] ModelBindingResult dtoResult )
459
+ [ NotNull ] ModelBindingResult result )
432
460
{
433
461
var bindingFlags = BindingFlags . Instance | BindingFlags . Public | BindingFlags . IgnoreCase ;
434
- var property = bindingContext . ModelType . GetProperty (
435
- propertyMetadata . PropertyName ,
436
- bindingFlags ) ;
462
+ var property = bindingContext . ModelType . GetProperty ( propertyMetadata . PropertyName , bindingFlags ) ;
437
463
438
464
if ( property == null )
439
465
{
440
466
// Nothing to do if property does not exist.
441
467
return ;
442
468
}
443
469
444
- if ( ! property . CanWrite )
470
+ if ( ! result . IsModelSet )
445
471
{
446
- // Try to handle as a collection if property exists but is not settable.
447
- AddToProperty ( bindingContext , modelExplorer , property , dtoResult ) ;
472
+ // If we don't have a value, don't set it on the model and trounce a pre-initialized value.
448
473
return ;
449
474
}
450
475
451
- object value = null ;
452
- if ( dtoResult . IsModelSet )
453
- {
454
- value = dtoResult . Model ;
455
- }
456
-
457
- if ( ! dtoResult . IsModelSet )
476
+ if ( ! property . CanWrite )
458
477
{
459
- // If we don't have a value, don't set it on the model and trounce a pre-initialized
460
- // value.
478
+ // Try to handle as a collection if property exists but is not settable.
479
+ AddToProperty ( bindingContext , modelExplorer , property , result ) ;
461
480
return ;
462
481
}
463
482
483
+ var value = result . Model ;
464
484
try
465
485
{
466
486
propertyMetadata . PropertySetter ( bindingContext . Model , value ) ;
467
487
}
468
488
catch ( Exception exception )
469
489
{
470
- AddModelError ( exception , bindingContext , dtoResult ) ;
490
+ AddModelError ( exception , bindingContext , result ) ;
471
491
}
472
492
}
473
493
474
494
private void AddToProperty (
475
495
ModelBindingContext bindingContext ,
476
496
ModelExplorer modelExplorer ,
477
497
PropertyInfo property ,
478
- ModelBindingResult dtoResult )
498
+ ModelBindingResult result )
479
499
{
480
500
var propertyExplorer = modelExplorer . GetExplorerForProperty ( property . Name ) ;
481
501
482
502
var target = propertyExplorer . Model ;
483
- var source = dtoResult . Model ;
484
- if ( ! dtoResult . IsModelSet || target == null || source == null )
503
+ var source = result . Model ;
504
+ if ( target == null || source == null )
485
505
{
486
506
// Cannot copy to or from a null collection.
487
507
return ;
488
508
}
489
509
510
+ if ( target == source )
511
+ {
512
+ // Added to the target collection in BindPropertiesAsync().
513
+ return ;
514
+ }
515
+
490
516
// Determine T if this is an ICollection<T> property. No need for a T[] case because CanUpdateProperty()
491
517
// ensures property is either settable or not an array. Underlying assumption is that CanUpdateProperty()
492
518
// and SetProperty() are overridden together.
@@ -507,7 +533,7 @@ private void AddToProperty(
507
533
}
508
534
catch ( Exception exception )
509
535
{
510
- AddModelError ( exception , bindingContext , dtoResult ) ;
536
+ AddModelError ( exception , bindingContext , result ) ;
511
537
}
512
538
}
513
539
@@ -529,7 +555,7 @@ private static void CallPropertyAddRange<TElement>(object target, object source)
529
555
private static void AddModelError (
530
556
Exception exception ,
531
557
ModelBindingContext bindingContext ,
532
- ModelBindingResult dtoResult )
558
+ ModelBindingResult result )
533
559
{
534
560
var targetInvocationException = exception as TargetInvocationException ;
535
561
if ( targetInvocationException != null && targetInvocationException . InnerException != null )
@@ -539,7 +565,7 @@ private static void AddModelError(
539
565
540
566
// Do not add an error message if a binding error has already occurred for this property.
541
567
var modelState = bindingContext . ModelState ;
542
- var modelStateKey = dtoResult . Key ;
568
+ var modelStateKey = result . Key ;
543
569
var validationState = modelState . GetFieldValidationState ( modelStateKey ) ;
544
570
if ( validationState == ModelValidationState . Unvalidated )
545
571
{
0 commit comments