This repository has been archived by the owner on Dec 14, 2018. It is now read-only.
/
MutableObjectModelBinder.cs
610 lines (530 loc) · 26.3 KB
/
MutableObjectModelBinder.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
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// <see cref="IModelBinder"/> implementation for binding complex values.
/// </summary>
public class MutableObjectModelBinder : IModelBinder
{
private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod =
typeof(MutableObjectModelBinder).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyAddRange));
/// <inheritdoc />
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelMetadata))
{
return null;
}
var mutableObjectBinderContext = new MutableObjectBinderContext()
{
ModelBindingContext = bindingContext,
PropertyMetadata = GetMetadataForProperties(bindingContext).ToArray(),
};
if (!(await CanCreateModel(mutableObjectBinderContext)))
{
return null;
}
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
var model = GetModel(bindingContext);
var results = await BindPropertiesAsync(bindingContext, mutableObjectBinderContext.PropertyMetadata);
var validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
model);
// Post-processing e.g. property setters and hooking up validation.
bindingContext.Model = model;
ProcessResults(bindingContext, results, validationNode);
return new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true,
validationNode: validationNode);
}
/// <summary>
/// Gets an indication whether a property with the given <paramref name="propertyMetadata"/> can be updated.
/// </summary>
/// <param name="propertyMetadata"><see cref="ModelMetadata"/> for the property of interest.</param>
/// <returns><c>true</c> if the property can be updated; <c>false</c> otherwise.</returns>
/// <remarks>Should return <c>true</c> only for properties <see cref="SetProperty"/> can update.</remarks>
protected virtual bool CanUpdateProperty([NotNull] ModelMetadata propertyMetadata)
{
return CanUpdatePropertyInternal(propertyMetadata);
}
internal async Task<bool> CanCreateModel(MutableObjectBinderContext context)
{
var bindingContext = context.ModelBindingContext;
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 always 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 false;
}
// Create the object if:
// 1. It is a top level model and no later fallback (to empty prefix) will occur.
if (isTopLevelObject && !bindingContext.IsFirstChanceBinding)
{
return true;
}
// 2. If it is top level object and there are no properties to bind
if (isTopLevelObject && context.PropertyMetadata != null && context.PropertyMetadata.Count == 0)
{
return true;
}
// 3. Any of the model properties can be bound using a value provider.
if (await CanValueBindAnyModelProperties(context))
{
return true;
}
return false;
}
private async Task<bool> CanValueBindAnyModelProperties(MutableObjectBinderContext context)
{
// If there are no properties on the model, there is nothing to bind. We are here means this is not a top
// level object. So we return false.
if (context.PropertyMetadata == null || context.PropertyMetadata.Count == 0)
{
return false;
}
// We want to check to see if any of the properties of the model can be bound using the value providers,
// because that's all that MutableObjectModelBinder can handle.
//
// However, because a property might specify a custom binding source ([FromForm]), it's not correct
// for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName),
// because that may include ALL 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 not
// allowed to come from a value provider, so we skip it.
//
// 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.
//
// If any property meets the above conditions and has a value from valueproviders, then we'll
// create the model and try to bind it. OR if ALL properties of the model have a greedy source,
// then we go ahead and create it.
//
var isAnyPropertyEnabledForValueProviderBasedBinding = false;
foreach (var propertyMetadata in context.PropertyMetadata)
{
// This check will skip properties which are marked explicitly using a non value binder.
var bindingSource = propertyMetadata.BindingSource;
if (bindingSource == null || !bindingSource.IsGreedy)
{
isAnyPropertyEnabledForValueProviderBasedBinding = true;
var propertyModelName = ModelNames.CreatePropertyModelName(
context.ModelBindingContext.ModelName,
propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName);
var propertyModelBindingContext = ModelBindingContext.GetChildModelBindingContext(
context.ModelBindingContext,
propertyModelName,
propertyMetadata);
// If any property can return a true value.
if (await CanBindValue(propertyModelBindingContext))
{
return true;
}
}
}
if (!isAnyPropertyEnabledForValueProviderBasedBinding)
{
// Either there are no properties or all the properties are marked as
// a non value provider based marker.
// This would be the case when the model has all its properties annotated with
// a IBinderMetadata. We want to be able to create such a model.
return true;
}
return false;
}
private async Task<bool> CanBindValue(ModelBindingContext bindingContext)
{
var valueProvider = bindingContext.ValueProvider;
var bindingSource = bindingContext.BindingSource;
if (bindingSource != null && !bindingSource.IsGreedy)
{
var rootValueProvider =
bindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider;
if (rootValueProvider != null)
{
valueProvider = rootValueProvider.Filter(bindingSource);
if (valueProvider == null)
{
// Unable to find a value provider for this binding source. Binding will fail.
return false;
}
}
}
if (await valueProvider.ContainsPrefixAsync(bindingContext.ModelName))
{
return true;
}
return false;
}
private static bool CanBindType(ModelMetadata modelMetadata)
{
// Simple types cannot use this binder
if (!modelMetadata.IsComplexType)
{
return false;
}
if (modelMetadata.IsCollectionType)
{
return false;
}
return true;
}
internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata)
{
return !propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
}
private static bool CanUpdateReadOnlyProperty(Type propertyType)
{
// Value types have copy-by-value semantics, which prevents us from updating
// properties that are marked readonly.
if (propertyType.GetTypeInfo().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;
}
// Returned dictionary contains entries corresponding to properties against which binding was attempted. If
// binding failed, the entry's value will have IsModelSet == false. Binding is attempted for all elements of
// propertyMetadatas.
private async Task<IDictionary<ModelMetadata, ModelBindingResult>> BindPropertiesAsync(
ModelBindingContext bindingContext,
IEnumerable<ModelMetadata> propertyMetadatas)
{
var results = new Dictionary<ModelMetadata, ModelBindingResult>();
foreach (var propertyMetadata in propertyMetadatas)
{
var propertyModelName = ModelNames.CreatePropertyModelName(
bindingContext.ModelName,
propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName);
var childContext = ModelBindingContext.GetChildModelBindingContext(
bindingContext,
propertyModelName,
propertyMetadata);
// ModelBindingContext.Model property values may be non-null when invoked via TryUpdateModel(). 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.
if (propertyMetadata.PropertyGetter != null &&
propertyMetadata.IsComplexType &&
!propertyMetadata.ModelType.IsArray)
{
childContext.Model = propertyMetadata.PropertyGetter(bindingContext.Model);
}
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childContext);
if (result == null)
{
// Could not bind. Let ProcessResult() know explicitly.
result = new ModelBindingResult(model: null, key: propertyModelName, isModelSet: false);
}
results[propertyMetadata] = result;
}
return results;
}
/// <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>
protected virtual object CreateModel([NotNull] ModelBindingContext bindingContext)
{
// If the Activator 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.
return Activator.CreateInstance(bindingContext.ModelType);
}
/// <summary>
/// Get <see cref="ModelBindingContext.Model"/> if that property is not <c>null</c>. Otherwise activate a
/// new instance of <see cref="ModelBindingContext.ModelType"/>.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
protected virtual object GetModel([NotNull] ModelBindingContext bindingContext)
{
if (bindingContext.Model != null)
{
return bindingContext.Model;
}
return CreateModel(bindingContext);
}
/// <summary>
/// Gets the collection of <see cref="ModelMetadata"/> for properties this binder should update.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>Collection of <see cref="ModelMetadata"/> for properties this binder should update.</returns>
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(
[NotNull] ModelBindingContext bindingContext)
{
var validationInfo = GetPropertyValidationInfo(bindingContext);
var newPropertyFilter = GetPropertyFilter();
return bindingContext.ModelMetadata.Properties
.Where(propertyMetadata =>
newPropertyFilter(bindingContext, propertyMetadata.PropertyName) &&
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
CanUpdateProperty(propertyMetadata));
}
private static Func<ModelBindingContext, string, bool> GetPropertyFilter()
{
return (ModelBindingContext context, string propertyName) =>
{
var modelMetadataPredicate = context.ModelMetadata.PropertyBindingPredicateProvider?.PropertyFilter;
return
context.PropertyFilter(context, propertyName) &&
(modelMetadataPredicate == null || modelMetadataPredicate(context, propertyName));
};
}
internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext)
{
var validationInfo = new PropertyValidationInfo();
foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties)
{
var propertyName = propertyMetadata.PropertyName;
if (!propertyMetadata.IsBindingAllowed)
{
// Nothing to do here if binding is not allowed.
validationInfo.SkipProperties.Add(propertyName);
continue;
}
if (propertyMetadata.IsBindingRequired)
{
validationInfo.RequiredProperties.Add(propertyName);
}
}
return validationInfo;
}
// Internal for testing.
internal ModelValidationNode ProcessResults(
ModelBindingContext bindingContext,
IDictionary<ModelMetadata, ModelBindingResult> results,
ModelValidationNode validationNode)
{
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer =
metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model);
var validationInfo = GetPropertyValidationInfo(bindingContext);
// Eliminate provided properties from RequiredProperties; leaving just *missing* required properties.
var boundProperties = results.Where(p => p.Value.IsModelSet).Select(p => p.Key.PropertyName);
validationInfo.RequiredProperties.ExceptWith(boundProperties);
foreach (var missingRequiredProperty in validationInfo.RequiredProperties)
{
var propertyExplorer = modelExplorer.GetExplorerForProperty(missingRequiredProperty);
var propertyName = propertyExplorer.Metadata.BinderModelName ?? missingRequiredProperty;
var modelStateKey = ModelNames.CreatePropertyModelName(bindingContext.ModelName, propertyName);
bindingContext.ModelState.TryAddModelError(
modelStateKey,
Resources.FormatModelBinding_MissingBindRequiredMember(propertyName));
}
// For each property that BindPropertiesAsync() attempted to bind, call the setter, recording
// exceptions as necessary.
foreach (var entry in results)
{
var result = entry.Value;
if (result != null)
{
var propertyMetadata = entry.Key;
SetProperty(bindingContext, modelExplorer, propertyMetadata, result);
var propertyValidationNode = result.ValidationNode;
if (propertyValidationNode == null)
{
// Make sure that irrespective of whether the properties of the model were bound with a value,
// create a validation node so that these get validated.
propertyValidationNode = new ModelValidationNode(result.Key, entry.Key, result.Model);
}
validationNode.ChildNodes.Add(propertyValidationNode);
}
}
return validationNode;
}
/// <summary>
/// Updates a property in the current <see cref="ModelBindingContext.Model"/>.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <param name="modelExplorer">
/// The <see cref="ModelExplorer"/> for the model containing property to set.
/// </param>
/// <param name="propertyMetadata">The <see cref="ModelMetadata"/> for the property to set.</param>
/// <param name="result">The <see cref="ModelBindingResult"/> for the property's new value.</param>
/// <remarks>Should succeed in all cases that <see cref="CanUpdateProperty"/> returns <c>true</c>.</remarks>
protected virtual void SetProperty(
[NotNull] ModelBindingContext bindingContext,
[NotNull] ModelExplorer modelExplorer,
[NotNull] ModelMetadata propertyMetadata,
[NotNull] ModelBindingResult result)
{
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
var property = bindingContext.ModelType.GetProperty(propertyMetadata.PropertyName, bindingFlags);
if (property == null)
{
// Nothing to do if property does not exist.
return;
}
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 (!property.CanWrite)
{
// Try to handle as a collection if property exists but is not settable.
AddToProperty(bindingContext, modelExplorer, property, result);
return;
}
var value = result.Model;
try
{
propertyMetadata.PropertySetter(bindingContext.Model, value);
}
catch (Exception exception)
{
AddModelError(exception, bindingContext, result);
}
}
private void AddToProperty(
ModelBindingContext bindingContext,
ModelExplorer modelExplorer,
PropertyInfo property,
ModelBindingResult result)
{
var propertyExplorer = modelExplorer.GetExplorerForProperty(property.Name);
var target = propertyExplorer.Model;
var source = result.Model;
if (target == null || source == null)
{
// Cannot copy to or from a null collection.
return;
}
if (target == source)
{
// Added to the target collection in BindPropertiesAsync().
return;
}
// Determine T if this is an ICollection<T> property. No need for a T[] case because CanUpdateProperty()
// ensures property is either settable or not an array. Underlying assumption is that CanUpdateProperty()
// and SetProperty() are overridden together.
var collectionTypeArguments = ClosedGenericMatcher.ExtractGenericInterface(
propertyExplorer.ModelType,
typeof(ICollection<>))
?.GenericTypeArguments;
if (collectionTypeArguments == null)
{
// Not a collection model.
return;
}
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(collectionTypeArguments);
try
{
propertyAddRange.Invoke(obj: null, parameters: new[] { target, source });
}
catch (Exception exception)
{
AddModelError(exception, bindingContext, result);
}
}
// Called via reflection.
private static void CallPropertyAddRange<TElement>(object target, object source)
{
var targetCollection = (ICollection<TElement>)target;
var sourceCollection = source as IEnumerable<TElement>;
if (sourceCollection != null && !targetCollection.IsReadOnly)
{
targetCollection.Clear();
foreach (var item in sourceCollection)
{
targetCollection.Add(item);
}
}
}
private static void AddModelError(
Exception exception,
ModelBindingContext bindingContext,
ModelBindingResult result)
{
var targetInvocationException = exception as TargetInvocationException;
if (targetInvocationException != null && 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 modelStateKey = result.Key;
var validationState = modelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
modelState.AddModelError(modelStateKey, exception);
}
}
// Returns true if validator execution adds a model error.
private static bool RunValidator(
IModelValidator validator,
ModelBindingContext bindingContext,
ModelExplorer propertyExplorer,
string modelStateKey)
{
var validationContext = new ModelValidationContext(bindingContext, propertyExplorer);
var addedError = false;
foreach (var validationResult in validator.Validate(validationContext))
{
bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message);
addedError = true;
}
if (!addedError)
{
bindingContext.ModelState.MarkFieldValid(modelStateKey);
}
return addedError;
}
internal sealed class PropertyValidationInfo
{
public PropertyValidationInfo()
{
RequiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
SkipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
public HashSet<string> RequiredProperties { get; private set; }
public HashSet<string> SkipProperties { get; private set; }
}
}
}