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 /// 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. /// values in <see cref="IValueProvider"/> when no values are found using the <see cref="ModelName"/> prefix.
/// </summary> /// </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; } 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> /// <summary>
/// Gets or sets the <see cref="IValueProvider"/> associated with this context. /// Gets or sets the <see cref="IValueProvider"/> associated with this context.
/// </summary> /// </summary>

View File

@ -196,6 +196,7 @@ namespace Microsoft.AspNet.Mvc
bindingInfo, bindingInfo,
parameterName); parameterName);
modelBindingContext.IsTopLevelObject = true;
modelBindingContext.ModelState = modelState; modelBindingContext.ModelState = modelState;
modelBindingContext.ValueProvider = operationBindingContext.ValueProvider; modelBindingContext.ValueProvider = operationBindingContext.ValueProvider;
modelBindingContext.OperationBindingContext = operationBindingContext; 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 // 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 // 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. // consider the entire ModelName as a prefix.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null; var modelBindingKey = bindingContext.IsTopLevelObject ? string.Empty : bindingContext.ModelName;
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName;
var httpContext = bindingContext.OperationBindingContext.HttpContext; var httpContext = bindingContext.OperationBindingContext.HttpContext;

View File

@ -27,12 +27,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)) 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. // default 'empty' model and return it.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null; if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
var hasExplicitAlias = bindingContext.BinderModelName != null;
if (isTopLevelObject && (hasExplicitAlias || bindingContext.ModelName == string.Empty))
{ {
model = CreateEmptyCollection(); model = CreateEmptyCollection();

View File

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

View File

@ -62,12 +62,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
else 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. // default 'empty' model and return it.
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null; if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
var hasExplicitAlias = bindingContext.BinderModelName != null;
if (isTopLevelObject && (hasExplicitAlias || bindingContext.ModelName == string.Empty))
{ {
var model = new KeyValuePair<TKey, TValue>(); var model = new KeyValuePair<TKey, TValue>();

View File

@ -71,9 +71,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
internal async Task<bool> CanCreateModel(MutableObjectBinderContext context) internal async Task<bool> CanCreateModel(MutableObjectBinderContext context)
{ {
var bindingContext = context.ModelBindingContext; var bindingContext = context.ModelBindingContext;
var isTopLevelObject = bindingContext.IsTopLevelObject;
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var hasExplicitAlias = bindingContext.BinderModelName != null;
// If we get here the model is a complex object which was not directly bound by any previous model binder, // 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 // 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 // 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. // 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 // 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.) // the creation of top level object (this is also required for ModelBinderAttribute to work.)
var bindingSource = bindingContext.BindingSource; var bindingSource = bindingContext.BindingSource;
if (!isTopLevelObject && if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
bindingSource != null &&
bindingSource.IsGreedy)
{ {
return false; return false;
} }
// Create the object if : // Create the object if:
// 1. It is a top level model with an explicit user supplied prefix. // 1. It is a top level model and no later fallback (to empty prefix) will occur.
// In this case since it will never fallback to empty prefix, we need to create the model here. if (isTopLevelObject && !bindingContext.IsFirstChanceBinding)
if (isTopLevelObject && hasExplicitAlias)
{ {
return true; return true;
} }
// 2. It is a top level object and there is no model name ( Fallback to empty prefix case ). // 2. Any of the model properties can be bound using a value provider.
// 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.
if (await CanValueBindAnyModelProperties(context)) if (await CanValueBindAnyModelProperties(context))
{ {
return true; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@ -303,8 +300,9 @@ namespace Microsoft.AspNet.Mvc
ModelState = modelState, ModelState = modelState,
ValueProvider = valueProvider, ValueProvider = valueProvider,
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
OperationBindingContext = operationBindingContext, OperationBindingContext = operationBindingContext,
PropertyFilter = predicate PropertyFilter = predicate,
}; };
var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext); var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext);

View File

@ -37,13 +37,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task ArrayModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass() public async Task ArrayModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[])); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
@ -58,13 +63,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback() public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[])); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
@ -78,37 +86,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Empty(Assert.IsType<string[]>(result.Model)); Assert.Empty(Assert.IsType<string[]>(result.Model));
Assert.Equal(string.Empty, result.Key); Assert.Equal("modelName", 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.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model); Assert.Same(result.ValidationNode.Model, result.Model);
@ -119,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory] [Theory]
[InlineData("")] [InlineData("")]
[InlineData("param")] [InlineData("param")]
public async Task ArrayModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix) public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>();

View File

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

View File

@ -218,13 +218,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task CollectionModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass() public async Task CollectionModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
@ -239,13 +244,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task CollectionModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback() public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
@ -259,37 +267,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Empty(Assert.IsType<List<string>>(result.Model)); Assert.Empty(Assert.IsType<List<string>>(result.Model));
Assert.Equal(string.Empty, result.Key); Assert.Equal("modelName", 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.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model); Assert.Same(result.ValidationNode.Model, result.Model);
@ -300,7 +278,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory] [Theory]
[InlineData("")] [InlineData("")]
[InlineData("param")] [InlineData("param")]
public async Task CollectionModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix) public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>();

View File

@ -65,13 +65,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task DictionaryModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass() public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
@ -86,13 +91,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback() public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
@ -106,37 +114,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Empty(Assert.IsType<Dictionary<string, string>>(result.Model)); Assert.Empty(Assert.IsType<Dictionary<string, string>>(result.Model));
Assert.Equal(string.Empty, result.Key); Assert.Equal("modelName", 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.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Same(result.ValidationNode.Model, result.Model); Assert.Same(result.ValidationNode.Model, result.Model);
@ -147,7 +125,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory] [Theory]
[InlineData("")] [InlineData("")]
[InlineData("param")] [InlineData("param")]
public async Task DictionaryModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix) public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>();

View File

@ -148,13 +148,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_ForTopLevelModel_OnFirstPass() public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>));
@ -169,13 +174,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_ForTopLevelModel_OnFallback() public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext(); 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; var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>)); context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(KeyValuePair<string, string>));
@ -189,37 +197,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(default(KeyValuePair<string, string>), Assert.IsType<KeyValuePair<string, string>>(result.Model)); Assert.Equal(default(KeyValuePair<string, string>), Assert.IsType<KeyValuePair<string, string>>(result.Model));
Assert.Equal(string.Empty, result.Key); Assert.Equal("modelName", 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.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal(result.ValidationNode.Model, result.Model); Assert.Equal(result.ValidationNode.Model, result.Model);
@ -230,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
[Theory] [Theory]
[InlineData("")] [InlineData("")]
[InlineData("param")] [InlineData("param")]
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_ForNonTopLevelModel(string prefix) public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>();

View File

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

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.AspNet.DataProtection; 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. // Need to have a key here so that the GenericModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
{ request => request.QueryString = new QueryString("?parameter.index=0"),
request.QueryString = new QueryString("?parameter.index=0"); options => options.ModelBinders.Add(new AddressBinder()));
});
var modelState = new ModelStateDictionary(); var modelState = new ModelStateDictionary();
@ -204,6 +203,41 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelState.IsValid); 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 // This is part of a random sampling of scenarios where a GenericModelBinder is used
// recursively. // recursively.
[Fact] [Fact]

View File

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