`CompositeModelBinder.TryBind()` should return `null` more often

- #2129
- do not propagate results with `!IsModelSet`, allowing empty prefix fallback
- adjust `ComplexModelDtoModelBinder` to at least fake-bind all properties
 - default values not consistently picked up otherwise

nit: correct 2 test names in `KeyValuePairModelBinderTest`
This commit is contained in:
Doug Bunting 2015-03-20 13:06:40 -07:00
parent bcb2fa6ba2
commit 94e326f953
5 changed files with 32 additions and 19 deletions

View File

@ -15,27 +15,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return null; return null;
} }
ModelBindingHelper.ValidateBindingContext(bindingContext, ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(ComplexModelDto), allowNullModel: false);
typeof(ComplexModelDto),
allowNullModel: false);
var dto = (ComplexModelDto)bindingContext.Model; var dto = (ComplexModelDto)bindingContext.Model;
foreach (var propertyMetadata in dto.PropertyMetadata) foreach (var propertyMetadata in dto.PropertyMetadata)
{ {
var propertyModelName = ModelBindingHelper.CreatePropertyModelName( var propertyModelName = ModelBindingHelper.CreatePropertyModelName(
bindingContext.ModelName, bindingContext.ModelName,
propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName); propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName);
var propertyBindingContext = ModelBindingContext.GetChildModelBindingContext( var propertyBindingContext = ModelBindingContext.GetChildModelBindingContext(
bindingContext, bindingContext,
propertyModelName, propertyModelName,
propertyMetadata); propertyMetadata);
// bind and propagate the values
// If we can't bind then leave the result missing (don't add a null).
var modelBindingResult = var modelBindingResult =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext); await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext);
if (modelBindingResult != null) if (modelBindingResult == null)
{
// Could not bind. Add a result so MutableObjectModelBinder will check for [DefaultValue].
dto.Results[propertyMetadata] =
new ModelBindingResult(model: null, key: propertyModelName, isModelSet: false);
}
else
{ {
dto.Results[propertyMetadata] = modelBindingResult; dto.Results[propertyMetadata] = modelBindingResult;
} }

View File

@ -97,7 +97,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
if (result != null) if (result != null)
{ {
return result; // Use returned ModelBindingResult if it either indicates the model was set or is related to a
// ModelState entry. The second condition is necessary because the ModelState entry would never be
// validated if caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid.
//
// In most (hopefully all) cases, the ModelState entry exists because some binders add errors
// before returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsModelSet || bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
{
return result;
}
// Current binder should have been able to bind value but found nothing. Exit loop in a way that
// tells caller to fall back to the empty prefix, if appropriate. Do not return result because it
// means only "other binders are not applicable".
break;
} }
} }

View File

@ -410,7 +410,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
} }
// for each property that was bound, call the setter, recording exceptions as necessary // For each property that ComplexModelDtoModelBinder attempted to bind, call the setter, recording
// exceptions as necessary.
foreach (var entry in dto.Results) foreach (var entry in dto.Results)
{ {
var dtoResult = entry.Value; var dtoResult = entry.Value;

View File

@ -112,10 +112,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task ModelBinder_ReturnsTrue_WithoutSettingValue() public async Task ModelBinder_ReturnsNull_IfBinderMatchesButDoesNotSetModel()
{ {
// Arrange // Arrange
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
@ -135,7 +134,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var modelBinder = new Mock<IModelBinder>(); var modelBinder = new Mock<IModelBinder>();
modelBinder modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>())) .Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(new ModelBindingResult(null, "someName", false))); .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false)));
var composite = CreateCompositeBinder(modelBinder.Object); var composite = CreateCompositeBinder(modelBinder.Object);
@ -143,10 +142,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await composite.BindModelAsync(bindingContext); var result = await composite.BindModelAsync(bindingContext);
// Assert // Assert
Assert.NotNull(result); Assert.Null(result);
Assert.False(result.IsModelSet);
Assert.Equal("someName", result.Key);
Assert.Null(result.Model);
} }
[Fact] [Fact]

View File

@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public class KeyValuePairModelBinderTest public class KeyValuePairModelBinderTest
{ {
[Fact] [Fact]
public async Task BindModel_MissingKey_ReturnsTrue_AndAddsModelValidationError() public async Task BindModel_MissingKey_ReturnsResult_AndAddsModelValidationError()
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); var valueProvider = new SimpleHttpValueProvider();
@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task BindModel_MissingValue_ReturnsTrue_AndAddsModelValidationError() public async Task BindModel_MissingValue_ReturnsResult_AndAddsModelValidationError()
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); var valueProvider = new SimpleHttpValueProvider();