3
3
4
4
using System ;
5
5
using System . Collections . Generic ;
6
+ using System . Diagnostics ;
6
7
using System . Linq . Expressions ;
7
8
using System . Reflection ;
8
9
using System . Threading . Tasks ;
@@ -17,6 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
17
18
/// </summary>
18
19
public class ComplexTypeModelBinder : IModelBinder
19
20
{
21
+ // Don't want a new public enum because communication between the private and internal methods of this class
22
+ // should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.
23
+
24
+ // Model contains only properties that are expected to bind from value providers and no value provider has
25
+ // matching data.
26
+ internal const int NoDataAvailable = 0 ;
27
+ // If model contains properties that are expected to bind from value providers, no value provider has matching
28
+ // data. Remaining (greedy) properties might bind successfully.
29
+ internal const int GreedyPropertiesMayHaveData = 1 ;
30
+ // Model contains at least one property that is expected to bind from value providers and a value provider has
31
+ // matching data.
32
+ internal const int ValueProviderDataAvailable = 2 ;
33
+
20
34
private readonly IDictionary < ModelMetadata , IModelBinder > _propertyBinders ;
21
35
private readonly ILogger _logger ;
22
36
private Func < object > _modelCreator ;
@@ -76,18 +90,21 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
76
90
77
91
_logger . AttemptingToBindModel ( bindingContext ) ;
78
92
79
- if ( ! CanCreateModel ( bindingContext ) )
93
+ var propertyData = CanCreateModel ( bindingContext ) ;
94
+ if ( propertyData == NoDataAvailable )
80
95
{
81
96
return Task . CompletedTask ;
82
97
}
83
98
84
99
// Perf: separated to avoid allocating a state machine when we don't
85
100
// need to go async.
86
- return BindModelCoreAsync ( bindingContext ) ;
101
+ return BindModelCoreAsync ( bindingContext , propertyData ) ;
87
102
}
88
103
89
- private async Task BindModelCoreAsync ( ModelBindingContext bindingContext )
104
+ private async Task BindModelCoreAsync ( ModelBindingContext bindingContext , int propertyData )
90
105
{
106
+ Debug . Assert ( propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable ) ;
107
+
91
108
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
92
109
if ( bindingContext . Model == null )
93
110
{
@@ -96,6 +113,8 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
96
113
97
114
var modelMetadata = bindingContext . ModelMetadata ;
98
115
var attemptedPropertyBinding = false ;
116
+ var propertyBindingSucceeded = false ;
117
+ var postponePlaceholderBinding = false ;
99
118
for ( var i = 0 ; i < modelMetadata . Properties . Count ; i ++ )
100
119
{
101
120
var property = modelMetadata . Properties [ i ] ;
@@ -104,42 +123,62 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
104
123
continue ;
105
124
}
106
125
107
- // Pass complex (including collection) values down so that binding system does not unnecessarily
108
- // recreate instances or overwrite inner properties that are not bound. No need for this with simple
109
- // values because they will be overwritten if binding succeeds. Arrays are never reused because they
110
- // cannot be resized.
111
- object propertyModel = null ;
112
- if ( property . PropertyGetter != null &&
113
- property . IsComplexType &&
114
- ! property . ModelType . IsArray )
126
+ if ( _propertyBinders [ property ] is PlaceholderBinder )
115
127
{
116
- propertyModel = property . PropertyGetter ( bindingContext . Model ) ;
128
+ if ( postponePlaceholderBinding )
129
+ {
130
+ // Decided to postpone binding properties that complete a loop in the model types when handling
131
+ // an earlier loop-completing property. Postpone binding this property too.
132
+ continue ;
133
+ }
134
+ else if ( ! bindingContext . IsTopLevelObject &&
135
+ ! propertyBindingSucceeded &&
136
+ propertyData == GreedyPropertiesMayHaveData )
137
+ {
138
+ // Have no confirmation of data for the current instance. Postpone completing the loop until
139
+ // we _know_ the current instance is useful. Recursion would otherwise occur prior to the
140
+ // block with a similar condition after the loop.
141
+ //
142
+ // Example cases include an Employee class containing
143
+ // 1. a Manager property of type Employee
144
+ // 2. an Employees property of type IList<Employee>
145
+ postponePlaceholderBinding = true ;
146
+ continue ;
147
+ }
117
148
}
118
149
119
150
var fieldName = property . BinderModelName ?? property . PropertyName ;
120
151
var modelName = ModelNames . CreatePropertyModelName ( bindingContext . ModelName , fieldName ) ;
121
-
122
- ModelBindingResult result ;
123
- using ( bindingContext . EnterNestedScope (
124
- modelMetadata : property ,
125
- fieldName : fieldName ,
126
- modelName : modelName ,
127
- model : propertyModel ) )
128
- {
129
- await BindProperty ( bindingContext ) ;
130
- result = bindingContext . Result ;
131
- }
152
+ var result = await BindProperty ( bindingContext , property , fieldName , modelName ) ;
132
153
133
154
if ( result . IsModelSet )
134
155
{
135
156
attemptedPropertyBinding = true ;
136
- SetProperty ( bindingContext , modelName , property , result ) ;
157
+ propertyBindingSucceeded = true ;
137
158
}
138
159
else if ( property . IsBindingRequired )
139
160
{
140
161
attemptedPropertyBinding = true ;
141
- var message = property . ModelBindingMessageProvider . MissingBindRequiredValueAccessor ( fieldName ) ;
142
- bindingContext . ModelState . TryAddModelError ( modelName , message ) ;
162
+ }
163
+ }
164
+
165
+ if ( postponePlaceholderBinding && propertyBindingSucceeded )
166
+ {
167
+ // Have some data for this instance. Continue with the model type loop.
168
+ for ( var i = 0 ; i < modelMetadata . Properties . Count ; i ++ )
169
+ {
170
+ var property = modelMetadata . Properties [ i ] ;
171
+ if ( ! CanBindProperty ( bindingContext , property ) )
172
+ {
173
+ continue ;
174
+ }
175
+
176
+ if ( _propertyBinders [ property ] is PlaceholderBinder )
177
+ {
178
+ var fieldName = property . BinderModelName ?? property . PropertyName ;
179
+ var modelName = ModelNames . CreatePropertyModelName ( bindingContext . ModelName , fieldName ) ;
180
+ await BindProperty ( bindingContext , property , fieldName , modelName ) ;
181
+ }
143
182
}
144
183
}
145
184
@@ -157,8 +196,37 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
157
196
bindingContext . ModelState . TryAddModelError ( bindingContext . ModelName , message ) ;
158
197
}
159
198
160
- bindingContext . Result = ModelBindingResult . Success ( bindingContext . Model ) ;
161
199
_logger . DoneAttemptingToBindModel ( bindingContext ) ;
200
+
201
+ // Have all binders failed because no data was available?
202
+ //
203
+ // If CanCreateModel determined a property has data, failures are likely due to conversion errors. For
204
+ // example, user may submit ?[0].id=twenty&[1].id=twenty-one&[2].id=22 for a collection of a complex type
205
+ // with an int id property. In that case, the bound model should be [ {}, {}, { id = 22 }] and
206
+ // ModelState should contain errors about both [0].id and [1].id. Do not inform higher-level binders of the
207
+ // failure in this and similar cases.
208
+ //
209
+ // If CanCreateModel could not find data for non-greedy properties, failures indicate greedy binders were
210
+ // unsuccessful. For example, user may submit file attachments [0].File and [1].File but not [2].File for
211
+ // a collection of a complex type containing an IFormFile property. In that case, we have exhausted the
212
+ // attached files and checking for [3].File is likely be pointless. (And, if it had a point, would we stop
213
+ // after 10 failures, 100, or more -- all adding redundant errors to ModelState?) Inform higher-level
214
+ // binders of the failure.
215
+ //
216
+ // Required properties do not change the logic below. Missed required properties cause ModelState errors
217
+ // but do not necessarily prevent further attempts to bind.
218
+ //
219
+ // This logic is intended to maximize correctness but does not avoid infinite loops or recursion when a
220
+ // greedy model binder succeeds unconditionally.
221
+ if ( ! bindingContext . IsTopLevelObject &&
222
+ ! propertyBindingSucceeded &&
223
+ propertyData == GreedyPropertiesMayHaveData )
224
+ {
225
+ bindingContext . Result = ModelBindingResult . Failed ( ) ;
226
+ return ;
227
+ }
228
+
229
+ bindingContext . Result = ModelBindingResult . Success ( bindingContext . Model ) ;
162
230
}
163
231
164
232
/// <summary>
@@ -194,6 +262,48 @@ protected virtual bool CanBindProperty(ModelBindingContext bindingContext, Model
194
262
return true ;
195
263
}
196
264
265
+ private async Task < ModelBindingResult > BindProperty (
266
+ ModelBindingContext bindingContext ,
267
+ ModelMetadata property ,
268
+ string fieldName ,
269
+ string modelName )
270
+ {
271
+ // Pass complex (including collection) values down so that binding system does not unnecessarily
272
+ // recreate instances or overwrite inner properties that are not bound. No need for this with simple
273
+ // values because they will be overwritten if binding succeeds. Arrays are never reused because they
274
+ // cannot be resized.
275
+ object propertyModel = null ;
276
+ if ( property . PropertyGetter != null &&
277
+ property . IsComplexType &&
278
+ ! property . ModelType . IsArray )
279
+ {
280
+ propertyModel = property . PropertyGetter ( bindingContext . Model ) ;
281
+ }
282
+
283
+ ModelBindingResult result ;
284
+ using ( bindingContext . EnterNestedScope (
285
+ modelMetadata : property ,
286
+ fieldName : fieldName ,
287
+ modelName : modelName ,
288
+ model : propertyModel ) )
289
+ {
290
+ await BindProperty ( bindingContext ) ;
291
+ result = bindingContext . Result ;
292
+ }
293
+
294
+ if ( result . IsModelSet )
295
+ {
296
+ SetProperty ( bindingContext , modelName , property , result ) ;
297
+ }
298
+ else if ( property . IsBindingRequired )
299
+ {
300
+ var message = property . ModelBindingMessageProvider . MissingBindRequiredValueAccessor ( fieldName ) ;
301
+ bindingContext . ModelState . TryAddModelError ( modelName , message ) ;
302
+ }
303
+
304
+ return result ;
305
+ }
306
+
197
307
/// <summary>
198
308
/// Attempts to bind a property of the model.
199
309
/// </summary>
@@ -208,7 +318,7 @@ protected virtual Task BindProperty(ModelBindingContext bindingContext)
208
318
return binder . BindModelAsync ( bindingContext ) ;
209
319
}
210
320
211
- internal bool CanCreateModel ( ModelBindingContext bindingContext )
321
+ internal int CanCreateModel ( ModelBindingContext bindingContext )
212
322
{
213
323
var isTopLevelObject = bindingContext . IsTopLevelObject ;
214
324
@@ -227,33 +337,28 @@ internal bool CanCreateModel(ModelBindingContext bindingContext)
227
337
var bindingSource = bindingContext . BindingSource ;
228
338
if ( ! isTopLevelObject && bindingSource != null && bindingSource . IsGreedy )
229
339
{
230
- return false ;
340
+ return NoDataAvailable ;
231
341
}
232
342
233
343
// Create the object if:
234
344
// 1. It is a top level model.
235
345
if ( isTopLevelObject )
236
346
{
237
- return true ;
347
+ return ValueProviderDataAvailable ;
238
348
}
239
349
240
350
// 2. Any of the model properties can be bound.
241
- if ( CanBindAnyModelProperties ( bindingContext ) )
242
- {
243
- return true ;
244
- }
245
-
246
- return false ;
351
+ return CanBindAnyModelProperties ( bindingContext ) ;
247
352
}
248
353
249
- private bool CanBindAnyModelProperties ( ModelBindingContext bindingContext )
354
+ private int CanBindAnyModelProperties ( ModelBindingContext bindingContext )
250
355
{
251
356
// If there are no properties on the model, there is nothing to bind. We are here means this is not a top
252
357
// level object. So we return false.
253
358
if ( bindingContext . ModelMetadata . Properties . Count == 0 )
254
359
{
255
360
_logger . NoPublicSettableProperties ( bindingContext ) ;
256
- return false ;
361
+ return NoDataAvailable ;
257
362
}
258
363
259
364
// We want to check to see if any of the properties of the model can be bound using the value providers or
@@ -279,7 +384,7 @@ private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
279
384
// Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
280
385
// create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
281
386
// then we go ahead and create it.
282
- //
387
+ var hasGreedyBinders = false ;
283
388
for ( var i = 0 ; i < bindingContext . ModelMetadata . Properties . Count ; i ++ )
284
389
{
285
390
var propertyMetadata = bindingContext . ModelMetadata . Properties [ i ] ;
@@ -292,7 +397,8 @@ private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
292
397
var bindingSource = propertyMetadata . BindingSource ;
293
398
if ( bindingSource != null && bindingSource . IsGreedy )
294
399
{
295
- return true ;
400
+ hasGreedyBinders = true ;
401
+ continue ;
296
402
}
297
403
298
404
// Otherwise, check whether the (perhaps filtered) value providers have a match.
@@ -307,14 +413,19 @@ private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
307
413
// If any property can be bound from a value provider, then success.
308
414
if ( bindingContext . ValueProvider . ContainsPrefix ( bindingContext . ModelName ) )
309
415
{
310
- return true ;
416
+ return ValueProviderDataAvailable ;
311
417
}
312
418
}
313
419
}
314
420
421
+ if ( hasGreedyBinders )
422
+ {
423
+ return GreedyPropertiesMayHaveData ;
424
+ }
425
+
315
426
_logger . CannotBindToComplexType ( bindingContext ) ;
316
427
317
- return false ;
428
+ return NoDataAvailable ;
318
429
}
319
430
320
431
// Internal for tests
0 commit comments