Add `ModelBindingContext.IsFirstChanceBinding` and `IsTopLevelObject`

- cleanup duplicate code now that #2445 is fixed
- update unit tests using old `ModelBindingContext` setups
- fix (just) one integration test where `MutableObjectModelBinder` incorrectly calculated
  `isTopLevelObject` and returned a non-`null` model
- undo temporary changes in `BodyModelBinderTests` due to increased reliance on incorrect
  `isTopLevelObject` in #2445 fix

nits:
- combine tests that are now duplicates
- beef up coverage of some `MutableObjectModelBinderTest` cases
- remove unused `using`s
This commit is contained in:
Doug Bunting 2015-06-13 20:08:53 -07:00
parent a8c37e57de
commit a170a4e1e4
17 changed files with 183 additions and 249 deletions

View File

@ -174,8 +174,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Gets or sets a value that indicates whether the binder should use an empty prefix to look up
/// values in <see cref="IValueProvider"/> when no values are found using the <see cref="ModelName"/> prefix.
/// </summary>
/// <remarks>
/// Passed into the model binding system. Should not be <c>true</c> when <see cref="IsTopLevelObject"/> is
/// <c>false</c>.
/// </remarks>
public bool FallbackToEmptyPrefix { get; set; }
/// <summary>
/// Gets or sets an indication that the current binder is handling the top-level object.
/// </summary>
/// <remarks>Passed into the model binding system.</remarks>
public bool IsTopLevelObject { get; set; }
/// <summary>
/// Gets or sets an indication that the model binding system will make another binding attempt (e.g. fall back
/// to the empty prefix) after this one.
/// </summary>
/// <remarks>
/// Not passed into the model binding system but instead set by the top-level binder. With built-in binders,
/// <c>true</c> only in binders called directly from a
/// <c>Microsoft.AspNet.Mvc.ModelBinding.CompositeModelBinder</c> that was passed a
/// <see cref="ModelBindingContext"/> with <see cref="FallbackToEmptyPrefix"/> <c>true</c>. <c>false</c>
/// otherwise.
/// </remarks>
public bool IsFirstChanceBinding { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueProvider"/> associated with this context.
/// </summary>

View File

@ -196,6 +196,7 @@ namespace Microsoft.AspNet.Mvc
bindingInfo,
parameterName);
modelBindingContext.IsTopLevelObject = true;
modelBindingContext.ModelState = modelState;
modelBindingContext.ValueProvider = operationBindingContext.ValueProvider;
modelBindingContext.OperationBindingContext = operationBindingContext;

View File

@ -30,8 +30,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// the parameter name/a custom name. In all other cases (like when binding body to a property) we
// consider the entire ModelName as a prefix.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName;
var modelBindingKey = bindingContext.IsTopLevelObject ? string.Empty : bindingContext.ModelName;
var httpContext = bindingContext.OperationBindingContext.HttpContext;

View File

@ -27,12 +27,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
{
// If this is the fallback case, and we failed to find data as a top-level model, then generate a
// If this is the fallback case and we failed to find data for a top-level model, then generate a
// default 'empty' model and return it.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var hasExplicitAlias = bindingContext.BinderModelName != null;
if (isTopLevelObject && (hasExplicitAlias || bindingContext.ModelName == string.Empty))
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
{
model = CreateEmptyCollection();

View File

@ -34,12 +34,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
// Will there be a last chance (fallback) binding attempt?
var isFirstChanceBinding = bindingContext.FallbackToEmptyPrefix &&
!string.IsNullOrEmpty(bindingContext.ModelName);
var newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
newBindingContext.IsFirstChanceBinding = isFirstChanceBinding;
var modelBindingResult = await TryBind(newBindingContext);
if (modelBindingResult == null &&
bindingContext.FallbackToEmptyPrefix &&
!string.IsNullOrEmpty(bindingContext.ModelName))
if (modelBindingResult == null && isFirstChanceBinding)
{
// Fall back to empty prefix.
newBindingContext = CreateNewBindingContext(bindingContext, modelName: string.Empty);
@ -142,6 +145,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
BinderModelName = oldBindingContext.BinderModelName,
BindingSource = oldBindingContext.BindingSource,
BinderType = oldBindingContext.BinderType,
IsTopLevelObject = oldBindingContext.IsTopLevelObject,
};
newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);

View File

@ -62,12 +62,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
else
{
// If this is the fallback case, and we failed to find data as a top-level model, then generate a
// If this is the fallback case and we failed to find data for a top-level model, then generate a
// default 'empty' model and return it.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var hasExplicitAlias = bindingContext.BinderModelName != null;
if (isTopLevelObject && (hasExplicitAlias || bindingContext.ModelName == string.Empty))
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
{
var model = new KeyValuePair<TKey, TValue>();

View File

@ -71,9 +71,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
internal async Task<bool> CanCreateModel(MutableObjectBinderContext context)
{
var bindingContext = context.ModelBindingContext;
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var hasExplicitAlias = bindingContext.BinderModelName != null;
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
@ -83,34 +81,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// 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.
// 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)
if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
{
return false;
}
// Create the object if :
// 1. It is a top level model with an explicit user supplied prefix.
// In this case since it will never fallback to empty prefix, we need to create the model here.
if (isTopLevelObject && hasExplicitAlias)
// 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. It is a top level object and there is no model name ( Fallback to empty prefix case ).
// This is necessary as we do not want to depend on a value provider to contain an empty prefix.
if (isTopLevelObject && bindingContext.ModelName == string.Empty)
{
return true;
}
// 3. Any of the model properties can be bound using a value provider.
// 2. Any of the model properties can be bound using a value provider.
if (await CanValueBindAnyModelProperties(context))
{
return true;

View File

@ -2,10 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@ -303,8 +300,9 @@ namespace Microsoft.AspNet.Mvc
ModelState = modelState,
ValueProvider = valueProvider,
FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
OperationBindingContext = operationBindingContext,
PropertyFilter = predicate
PropertyFilter = predicate,
};
var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext);

View File

@ -37,13 +37,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task ArrayModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass()
public async Task ArrayModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{
// Arrange
var binder = new ArrayModelBinder<string>();
var context = CreateContext();
context.ModelName = "param";
context.IsFirstChanceBinding = true;
context.IsTopLevelObject = true;
// Explicit prefix and empty model name both ignored.
context.BinderModelName = "prefix";
context.ModelName = string.Empty;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
@ -58,13 +63,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback()
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{
// Arrange
var binder = new ArrayModelBinder<string>();
var context = CreateContext();
context.ModelName = string.Empty;
context.IsTopLevelObject = true;
// Lack of prefix and non-empty model name both ignored.
context.ModelName = "modelName";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
@ -78,37 +86,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result);
Assert.Empty(Assert.IsType<string[]>(result.Model));
Assert.Equal(string.Empty, result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
Assert.Same(result.ValidationNode.Key, result.Key);
Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata);
}
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_ForTopLevelModel_WithExplicitPrefix()
{
// Arrange
var binder = new ArrayModelBinder<string>();
var context = CreateContext();
context.ModelName = "prefix";
context.BinderModelName = "prefix";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.Empty(Assert.IsType<string[]>(result.Model));
Assert.Equal("prefix", result.Key);
Assert.Equal("modelName", result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
@ -119,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task ArrayModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix)
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new ArrayModelBinder<string>();

View File

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -279,6 +278,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = new ModelBindingContext
{
IsTopLevelObject = true,
ModelMetadata = metadataProvider.GetMetadataForType(modelType),
ModelName = "someName",
ValueProvider = Mock.Of<IValueProvider>(),

View File

@ -218,13 +218,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task CollectionModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass()
public async Task CollectionModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var context = CreateContext();
context.ModelName = "param";
context.IsTopLevelObject = true;
context.IsFirstChanceBinding = true;
// Explicit prefix and empty model name both ignored.
context.BinderModelName = "prefix";
context.ModelName = string.Empty;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
@ -239,13 +244,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback()
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var context = CreateContext();
context.ModelName = string.Empty;
context.IsTopLevelObject = true;
// Lack of prefix and non-empty model name both ignored.
context.ModelName = "modelName";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
@ -259,37 +267,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result);
Assert.Empty(Assert.IsType<List<string>>(result.Model));
Assert.Equal(string.Empty, result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
Assert.Same(result.ValidationNode.Key, result.Key);
Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata);
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollection_ForTopLevelModel_WithExplicitPrefix()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var context = CreateContext();
context.ModelName = "prefix";
context.BinderModelName = "prefix";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.Empty(Assert.IsType<List<string>>(result.Model));
Assert.Equal("prefix", result.Key);
Assert.Equal("modelName", result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
@ -300,7 +278,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task CollectionModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix)
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new CollectionModelBinder<string>();

View File

@ -65,13 +65,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task DictionaryModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass()
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
context.ModelName = "param";
context.IsTopLevelObject = true;
context.IsFirstChanceBinding = true;
// Explicit prefix and empty model name both ignored.
context.BinderModelName = "prefix";
context.ModelName = string.Empty;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
@ -86,13 +91,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback()
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
context.ModelName = string.Empty;
context.IsTopLevelObject = true;
// Lack of prefix and non-empty model name both ignored.
context.ModelName = "modelName";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
@ -106,37 +114,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result);
Assert.Empty(Assert.IsType<Dictionary<string, string>>(result.Model));
Assert.Equal(string.Empty, result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
Assert.Same(result.ValidationNode.Key, result.Key);
Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata);
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollection_ForTopLevelModel_WithExplicitPrefix()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
context.ModelName = "prefix";
context.BinderModelName = "prefix";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.Empty(Assert.IsType<Dictionary<string, string>>(result.Model));
Assert.Equal("prefix", result.Key);
Assert.Equal("modelName", result.Key);
Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model);
@ -147,7 +125,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task DictionaryModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix)
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();

View File

@ -148,13 +148,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass()
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext();
context.ModelName = "param";
context.IsTopLevelObject = true;
context.IsFirstChanceBinding = true;
// Explicit prefix and empty model name both ignored.
context.BinderModelName = "prefix";
context.ModelName = string.Empty;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>));
@ -169,13 +174,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback()
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext();
context.ModelName = string.Empty;
context.IsTopLevelObject = true;
// Lack of prefix and non-empty model name both ignored.
context.ModelName = "modelName";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>));
@ -189,37 +197,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result);
Assert.Equal(default(KeyValuePair<string, string>), Assert.IsType<KeyValuePair<string, string>>(result.Model));
Assert.Equal(string.Empty, result.Key);
Assert.True(result.IsModelSet);
Assert.Equal(result.ValidationNode.Model, result.Model);
Assert.Same(result.ValidationNode.Key, result.Key);
Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata);
}
[Fact]
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_ForTopLevelModel_WithExplicitPrefix()
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext();
context.ModelName = "prefix";
context.BinderModelName = "prefix";
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.Equal(default(KeyValuePair<string, string>), Assert.IsType<KeyValuePair<string, string>>(result.Model));
Assert.Equal("prefix", result.Key);
Assert.Equal("modelName", result.Key);
Assert.True(result.IsModelSet);
Assert.Equal(result.ValidationNode.Model, result.Model);
@ -230,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix)
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();

View File

@ -20,23 +20,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class MutableObjectModelBinderTest
{
[Theory]
[InlineData(typeof(Person), true)]
[InlineData(typeof(Person), false)]
[InlineData(typeof(EmptyModel), true)]
[InlineData(typeof(EmptyModel), false)]
public async Task CanCreateModel_CreatesModel_ForTopLevelObjectIfThereIsExplicitPrefix(
Type modelType,
bool isPrefixProvided)
[InlineData(true, true, "", "", false)]
[InlineData(true, false, "", "", true)]
[InlineData(false, true, "", "", false)] // !isTopLevelObject && isFirstChanceBinding cases are unexpected
[InlineData(false, false, "", "", false)]
[InlineData(true, true, "prefix", "", false)]
[InlineData(true, false, "prefix", "", true)]
[InlineData(false, true, "prefix", "", false)]
[InlineData(false, false, "prefix", "", false)]
[InlineData(true, true, "", "dummyModelName", false)]
[InlineData(true, false, "", "dummyModelName", true)]
[InlineData(false, true, "", "dummyModelName", false)]
[InlineData(false, false, "", "dummyModelName", false)]
[InlineData(true, true, "prefix", "dummyModelName", false)]
[InlineData(true, false, "prefix", "dummyModelName", true)]
[InlineData(false, true, "prefix", "dummyModelName", false)]
[InlineData(false, false, "prefix", "dummyModelName", false)]
public async Task CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndNotIsFirstChanceBinding(
bool isTopLevelObject,
bool isFirstChanceBinding,
string binderModelName,
string modelName,
bool expectedCanCreate)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
mockValueProvider
.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var metadataProvider = new TestModelMetadataProvider();
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
IsTopLevelObject = isTopLevelObject,
IsFirstChanceBinding = isFirstChanceBinding,
// Random type.
ModelMetadata = metadataProvider.GetMetadataForType(typeof(Person)),
ValueProvider = mockValueProvider.Object,
@ -47,65 +66,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
},
// Setting it to empty ensures that model does not get created becasue of no model name.
ModelName = "dummyModelName",
BinderModelName = isPrefixProvided ? "prefix" : null,
}
// CanCreateModel() ignores the BinderModelName and ModelName properties.
BinderModelName = binderModelName,
ModelName = modelName,
},
};
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
var canCreate = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(isPrefixProvided, retModel);
}
[Theory]
[InlineData(typeof(Person), true)]
[InlineData(typeof(Person), false)]
[InlineData(typeof(EmptyModel), true)]
[InlineData(typeof(EmptyModel), false)]
public async Task
CanCreateModel_CreatesModel_ForTopLevelObjectIfThereIsEmptyModelName(Type modelType, bool emptyModelName)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Random type.
ModelMetadata = GetMetadataForType(typeof(Person)),
ValueProvider = mockValueProvider.Object,
OperationBindingContext = new OperationBindingContext
{
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
ValueProvider = mockValueProvider.Object,
MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider()
}
}
};
bindingContext.ModelBindingContext.ModelName = emptyModelName ? string.Empty : "dummyModelName";
var mutableBinder = new TestableMutableObjectModelBinder();
bindingContext.PropertyMetadata = mutableBinder.GetMetadataForProperties(
bindingContext.ModelBindingContext);
// Act
var retModel = await mutableBinder.CanCreateModel(bindingContext);
// Assert
Assert.Equal(emptyModelName, retModel);
Assert.Equal(expectedCanCreate, canCreate);
}
[Fact]
public async Task CanCreateModel_ReturnsFalse_ForNonTopLevelModel_IfModelIsMarkedWithBinderMetadata()
public async Task CanCreateModel_ReturnsFalse_IfNotIsTopLevelObjectAndModelIsMarkedWithBinderMetadata()
{
// Get the property metadata so that it is not a top level object.
var modelMetadata = GetMetadataForType(typeof(Document))
@ -135,13 +114,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
[Fact]
public async Task CanCreateModel_ReturnsTrue_ForTopLevelModel_IfModelIsMarkedWithBinderMetadata()
public async Task CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndModelIsMarkedWithBinderMetadata()
{
var bindingContext = new MutableObjectBinderContext
{
ModelBindingContext = new ModelBindingContext
{
// Here the metadata represents a top level object.
IsTopLevelObject = true,
ModelMetadata = GetMetadataForType(typeof(Document)),
OperationBindingContext = new OperationBindingContext
{
@ -198,7 +179,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanCreateModel_ReturnsTrue_ForNonTopLevelModel_BasedOnValueAvailability(bool valueAvailable)
public async Task CanCreateModel_ReturnsTrue_IfNotIsTopLevelObject_BasedOnValueAvailability(
bool valueAvailable)
{
// Arrange
var mockValueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
@ -434,7 +416,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
[Fact]
public async Task BindModel_InitsInstance_ForEmptyModelName()
public async Task BindModel_InitsInstance_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{
// Arrange
var mockValueProvider = new Mock<IValueProvider>();
@ -444,6 +426,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var mockDtoBinder = new Mock<IModelBinder>();
var bindingContext = new ModelBindingContext
{
IsTopLevelObject = true,
ModelMetadata = GetMetadataForType(typeof(Person)),
ModelName = "",
ValueProvider = mockValueProvider.Object,

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.AspNet.DataProtection;

View File

@ -181,10 +181,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
};
// Need to have a key here so that the GenericModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.index=0");
});
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request.QueryString = new QueryString("?parameter.index=0"),
options => options.ModelBinders.Add(new AddressBinder()));
var modelState = new ModelStateDictionary();
@ -204,6 +203,41 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelState.IsValid);
}
// Similar to the GenericModelBinder_BindsCollection_ElementTypeUsesGreedyModelBinder_WithPrefix_Success
// scenario but mis-configured. Model using a BindingSource for which no ModelBinder is enabled.
[Fact]
public async Task GenericModelBinder_BindsCollection_ElementTypeUsesGreedyBindingSource_WithPrefix_NullElement()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Address[])
};
// Need to have a key here so that the GenericModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request.QueryString = new QueryString("?parameter.index=0"));
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Address[]>(modelBindingResult.Model);
Assert.Equal(1, model.Length);
Assert.Null(model[0]);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
// This is part of a random sampling of scenarios where a GenericModelBinder is used
// recursively.
[Fact]

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;