Skip to content

Commit 5bbf710

Browse files
authored
Fail ComplexTypeModelBinder after CanCreateModel(...) in some cases (#6793)
- #4802 and #6616 - also reduces the impact incorrect metadata as in #4939 - postpone some property binding in `ComplexTypeModelBinder`
1 parent de74a0e commit 5bbf710

File tree

4 files changed

+495
-97
lines changed

4 files changed

+495
-97
lines changed

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ComplexTypeModelBinder.cs

Lines changed: 153 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Linq.Expressions;
78
using System.Reflection;
89
using System.Threading.Tasks;
@@ -17,6 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
1718
/// </summary>
1819
public class ComplexTypeModelBinder : IModelBinder
1920
{
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+
2034
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
2135
private readonly ILogger _logger;
2236
private Func<object> _modelCreator;
@@ -76,18 +90,21 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
7690

7791
_logger.AttemptingToBindModel(bindingContext);
7892

79-
if (!CanCreateModel(bindingContext))
93+
var propertyData = CanCreateModel(bindingContext);
94+
if (propertyData == NoDataAvailable)
8095
{
8196
return Task.CompletedTask;
8297
}
8398

8499
// Perf: separated to avoid allocating a state machine when we don't
85100
// need to go async.
86-
return BindModelCoreAsync(bindingContext);
101+
return BindModelCoreAsync(bindingContext, propertyData);
87102
}
88103

89-
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
104+
private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int propertyData)
90105
{
106+
Debug.Assert(propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable);
107+
91108
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
92109
if (bindingContext.Model == null)
93110
{
@@ -96,6 +113,8 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
96113

97114
var modelMetadata = bindingContext.ModelMetadata;
98115
var attemptedPropertyBinding = false;
116+
var propertyBindingSucceeded = false;
117+
var postponePlaceholderBinding = false;
99118
for (var i = 0; i < modelMetadata.Properties.Count; i++)
100119
{
101120
var property = modelMetadata.Properties[i];
@@ -104,42 +123,62 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
104123
continue;
105124
}
106125

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)
115127
{
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+
}
117148
}
118149

119150
var fieldName = property.BinderModelName ?? property.PropertyName;
120151
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);
132153

133154
if (result.IsModelSet)
134155
{
135156
attemptedPropertyBinding = true;
136-
SetProperty(bindingContext, modelName, property, result);
157+
propertyBindingSucceeded = true;
137158
}
138159
else if (property.IsBindingRequired)
139160
{
140161
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+
}
143182
}
144183
}
145184

@@ -157,8 +196,37 @@ private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
157196
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
158197
}
159198

160-
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
161199
_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);
162230
}
163231

164232
/// <summary>
@@ -194,6 +262,48 @@ protected virtual bool CanBindProperty(ModelBindingContext bindingContext, Model
194262
return true;
195263
}
196264

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+
197307
/// <summary>
198308
/// Attempts to bind a property of the model.
199309
/// </summary>
@@ -208,7 +318,7 @@ protected virtual Task BindProperty(ModelBindingContext bindingContext)
208318
return binder.BindModelAsync(bindingContext);
209319
}
210320

211-
internal bool CanCreateModel(ModelBindingContext bindingContext)
321+
internal int CanCreateModel(ModelBindingContext bindingContext)
212322
{
213323
var isTopLevelObject = bindingContext.IsTopLevelObject;
214324

@@ -227,33 +337,28 @@ internal bool CanCreateModel(ModelBindingContext bindingContext)
227337
var bindingSource = bindingContext.BindingSource;
228338
if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
229339
{
230-
return false;
340+
return NoDataAvailable;
231341
}
232342

233343
// Create the object if:
234344
// 1. It is a top level model.
235345
if (isTopLevelObject)
236346
{
237-
return true;
347+
return ValueProviderDataAvailable;
238348
}
239349

240350
// 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);
247352
}
248353

249-
private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
354+
private int CanBindAnyModelProperties(ModelBindingContext bindingContext)
250355
{
251356
// If there are no properties on the model, there is nothing to bind. We are here means this is not a top
252357
// level object. So we return false.
253358
if (bindingContext.ModelMetadata.Properties.Count == 0)
254359
{
255360
_logger.NoPublicSettableProperties(bindingContext);
256-
return false;
361+
return NoDataAvailable;
257362
}
258363

259364
// 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)
279384
// Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
280385
// create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
281386
// then we go ahead and create it.
282-
//
387+
var hasGreedyBinders = false;
283388
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
284389
{
285390
var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
@@ -292,7 +397,8 @@ private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
292397
var bindingSource = propertyMetadata.BindingSource;
293398
if (bindingSource != null && bindingSource.IsGreedy)
294399
{
295-
return true;
400+
hasGreedyBinders = true;
401+
continue;
296402
}
297403

298404
// Otherwise, check whether the (perhaps filtered) value providers have a match.
@@ -307,14 +413,19 @@ private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
307413
// If any property can be bound from a value provider, then success.
308414
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
309415
{
310-
return true;
416+
return ValueProviderDataAvailable;
311417
}
312418
}
313419
}
314420

421+
if (hasGreedyBinders)
422+
{
423+
return GreedyPropertiesMayHaveData;
424+
}
425+
315426
_logger.CannotBindToComplexType(bindingContext);
316427

317-
return false;
428+
return NoDataAvailable;
318429
}
319430

320431
// Internal for tests

0 commit comments

Comments
 (0)