Change ModelBinding to use a single pass

This commit is contained in:
Ryan Nowak 2015-08-20 20:33:06 -07:00
parent 8a33972c09
commit 98b3f055e1
20 changed files with 367 additions and 279 deletions

View File

@ -11,9 +11,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public class ModelBindingContext public class ModelBindingContext
{ {
private static readonly Func<ModelBindingContext, string, bool>
_defaultPropertyFilter = (context, propertyName) => true;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModelBindingContext"/> class. /// Initializes a new instance of the <see cref="ModelBindingContext"/> class.
/// </summary> /// </summary>
@ -167,19 +164,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <remarks>Passed into the model binding system.</remarks> /// <remarks>Passed into the model binding system.</remarks>
public bool IsTopLevelObject { get; set; } 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>
@ -191,4 +175,4 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public Func<ModelBindingContext, string, bool> PropertyFilter { get; set; } public Func<ModelBindingContext, string, bool> PropertyFilter { get; set; }
} }
} }

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public ModelBindingResult(string key) public ModelBindingResult(string key)
{ {
Key = key; Key = key;
IsFatalError = true;
} }
/// <summary> /// <summary>
@ -62,12 +61,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public string Key { get; } public string Key { get; }
/// <summary>
/// Gets a value indicating the caller should not attempt binding again. This attempt encountered a fatal
/// error.
/// </summary>
public bool IsFatalError { get; }
/// <summary> /// <summary>
/// <para> /// <para>
/// Gets a value indicating whether or not the <see cref="Model"/> value has been set. /// Gets a value indicating whether or not the <see cref="Model"/> value has been set.

View File

@ -29,9 +29,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var model = bindingContext.Model; var model = bindingContext.Model;
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{ {
// If this is the fallback case and we failed to find data for a top-level model, then generate a // If we failed to find data for a top-level model, then generate a
// default 'empty' model (or use existing Model) and return it. // default 'empty' model (or use existing Model) and return it.
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject) if (bindingContext.IsTopLevelObject)
{ {
if (model == null) if (model == null)
{ {

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Core;
@ -35,29 +34,14 @@ 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 newBindingContext = CreateNewBindingContext(bindingContext);
var isFirstChanceBinding = bindingContext.FallbackToEmptyPrefix &&
!string.IsNullOrEmpty(bindingContext.ModelName);
var newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
if (newBindingContext == null) if (newBindingContext == null)
{ {
// Unable to find a value provider for this binding source. Binding will fail. // Unable to find a value provider for this binding source. Binding will fail.
return null; return null;
} }
newBindingContext.IsFirstChanceBinding = isFirstChanceBinding; var modelBindingResult = await RunModelBinders(newBindingContext);
var modelBindingResult = await TryBind(newBindingContext);
if (modelBindingResult == null && isFirstChanceBinding)
{
// Fall back to empty prefix.
newBindingContext = CreateNewBindingContext(bindingContext, modelName: string.Empty);
Debug.Assert(newBindingContext != null, "Should have failed on first attempt.");
modelBindingResult = await TryBind(newBindingContext);
}
if (modelBindingResult == null) if (modelBindingResult == null)
{ {
// Unable to bind or something went wrong. // Unable to bind or something went wrong.
@ -99,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelBindingResult.ValidationNode); modelBindingResult.ValidationNode);
} }
private async Task<ModelBindingResult> TryBind(ModelBindingContext bindingContext) private async Task<ModelBindingResult> RunModelBinders(ModelBindingContext bindingContext)
{ {
RuntimeHelpers.EnsureSufficientExecutionStack(); RuntimeHelpers.EnsureSufficientExecutionStack();
@ -108,18 +92,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
if (result != null) if (result != null)
{ {
// Use returned ModelBindingResult if it indicates the model was set, indicates the binder // This condition is necessary because the ModelState entry would never be validated if
// encountered a fatal error, or is related to a ModelState entry.
//
// The second condition is necessary because the BodyModelBinder unconditionally binds during the
// first attempt and does not always create ModelState values using ModelName.
//
// The third 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 // 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 // (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. // returning a result with !IsModelSet. Those binders often cannot run twice anyhow.
if (result.IsFatalError || if (result.IsModelSet ||
result.IsModelSet ||
bindingContext.ModelState.ContainsKey(bindingContext.ModelName)) bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
{ {
return result; return result;
@ -136,26 +113,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return null; return null;
} }
private static ModelBindingContext CreateNewBindingContext( private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext)
ModelBindingContext oldBindingContext,
string modelName)
{ {
var newBindingContext = new ModelBindingContext
{
Model = oldBindingContext.Model,
ModelMetadata = oldBindingContext.ModelMetadata,
ModelName = modelName,
FieldName = oldBindingContext.FieldName,
ModelState = oldBindingContext.ModelState,
ValueProvider = oldBindingContext.ValueProvider,
OperationBindingContext = oldBindingContext.OperationBindingContext,
PropertyFilter = oldBindingContext.PropertyFilter,
BinderModelName = oldBindingContext.BinderModelName,
BindingSource = oldBindingContext.BindingSource,
BinderType = oldBindingContext.BinderType,
IsTopLevelObject = oldBindingContext.IsTopLevelObject,
};
// If the property has a specified data binding sources, we need to filter the set of value providers // If the property has a specified data binding sources, we need to filter the set of value providers
// to just those that match. We can skip filtering when IsGreedy == true, because that can't use // to just those that match. We can skip filtering when IsGreedy == true, because that can't use
// value providers. // value providers.
@ -174,15 +133,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// public IActionResult UpdatePerson([FromForm] Person person) { } // public IActionResult UpdatePerson([FromForm] Person person) { }
// //
// In this example, [FromQuery] overrides the ambient data source (form). // In this example, [FromQuery] overrides the ambient data source (form).
IValueProvider valueProvider = oldBindingContext.ValueProvider;
var bindingSource = oldBindingContext.BindingSource; var bindingSource = oldBindingContext.BindingSource;
if (bindingSource != null && !bindingSource.IsGreedy) if (bindingSource != null && !bindingSource.IsGreedy)
{ {
var valueProvider = var bindingSourceValueProvider = valueProvider as IBindingSourceValueProvider;
oldBindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider; if (bindingSourceValueProvider != null)
if (valueProvider != null)
{ {
newBindingContext.ValueProvider = valueProvider.Filter(bindingSource); valueProvider = bindingSourceValueProvider.Filter(bindingSource);
if (newBindingContext.ValueProvider == null) if (valueProvider == null)
{ {
// Unable to find a value provider for this binding source. // Unable to find a value provider for this binding source.
return null; return null;
@ -190,6 +149,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
} }
var newBindingContext = new ModelBindingContext
{
Model = oldBindingContext.Model,
ModelMetadata = oldBindingContext.ModelMetadata,
FieldName = oldBindingContext.FieldName,
ModelState = oldBindingContext.ModelState,
ValueProvider = valueProvider,
OperationBindingContext = oldBindingContext.OperationBindingContext,
PropertyFilter = oldBindingContext.PropertyFilter,
BinderModelName = oldBindingContext.BinderModelName,
BindingSource = oldBindingContext.BindingSource,
BinderType = oldBindingContext.BinderType,
IsTopLevelObject = oldBindingContext.IsTopLevelObject,
};
if (bindingSource != null && bindingSource.IsGreedy)
{
newBindingContext.ModelName = oldBindingContext.ModelName;
}
else if (
!oldBindingContext.FallbackToEmptyPrefix ||
newBindingContext.ValueProvider.ContainsPrefix(oldBindingContext.ModelName))
{
newBindingContext.ModelName = oldBindingContext.ModelName;
}
else
{
newBindingContext.ModelName = string.Empty;
}
return newBindingContext; return newBindingContext;
} }
} }

View File

@ -69,6 +69,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
var form = await request.ReadFormAsync(); var form = await request.ReadFormAsync();
// If we're at the top level, then use the FieldName (paramter or property name).
// This handles the fact that there will be nothing in the ValueProviders for this parameter
// and so we'll do the right thing even though we 'fell-back' to the empty prefix.
var modelName = bindingContext.IsTopLevelObject
? bindingContext.FieldName
: bindingContext.ModelName;
foreach (var file in form.Files) foreach (var file in form.Files)
{ {
ContentDispositionHeaderValue parsedContentDisposition; ContentDispositionHeaderValue parsedContentDisposition;
@ -82,8 +89,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
continue; continue;
} }
var modelName = HeaderUtilities.RemoveQuotes(parsedContentDisposition.Name); var fileName = HeaderUtilities.RemoveQuotes(parsedContentDisposition.Name);
if (modelName.Equals(bindingContext.ModelName, StringComparison.OrdinalIgnoreCase)) if (fileName.Equals(modelName, StringComparison.OrdinalIgnoreCase))
{ {
postedFiles.Add(file); postedFiles.Add(file);
} }

View File

@ -62,9 +62,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
else else
{ {
// If this is the fallback case and we failed to find data for a top-level model, then generate a // If 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.
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject) if (bindingContext.IsTopLevelObject)
{ {
var model = new KeyValuePair<TKey, TValue>(); var model = new KeyValuePair<TKey, TValue>();

View File

@ -96,8 +96,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
// Create the object if: // Create the object if:
// 1. It is a top level model and no later fallback (to empty prefix) will occur. // 1. It is a top level model.
if (isTopLevelObject && !bindingContext.IsFirstChanceBinding) if (isTopLevelObject)
{ {
return true; return true;
} }

View File

@ -37,33 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task ArrayModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding() public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new ArrayModelBinder<string>();
var context = CreateContext();
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[]));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Null(result);
}
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>(); var binder = new ArrayModelBinder<string>();

View File

@ -76,7 +76,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns non-null because it understands the metadata type. // Returns non-null because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
@ -103,7 +102,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
} }
@ -172,7 +170,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns non-null because it understands the metadata type. // Returns non-null because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
@ -209,7 +206,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns non-null result because it understands the metadata type. // Returns non-null result because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsFatalError);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.ValidationNode);

View File

@ -227,33 +227,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task CollectionModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding() public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new CollectionModelBinder<string>();
var context = CreateContext();
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>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Null(result);
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>(); var binder = new CollectionModelBinder<string>();
@ -284,7 +258,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata);
} }
// Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding except // Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject except
// Model already has a value. // Model already has a value.
[Fact] [Fact]
public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull() public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull()

View File

@ -6,7 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Moq; using Moq;
using Xunit; using Xunit;
@ -65,6 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName", ModelName = "someName",
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
@ -136,49 +136,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Null(result); Assert.Null(result);
} }
[Fact]
public async Task ModelBinder_FallsBackToEmpty_IfBinderMatchesButDoesNotSetModel()
{
// Arrange
var bindingContext = new ModelBindingContext
{
FallbackToEmptyPrefix = true,
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
ModelName = "someName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider
{
{ "someOtherName", "dummyValue" }
},
};
var count = 0;
var modelBinder = new Mock<IModelBinder>();
modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(context =>
{
// Expect two calls; the second with empty ModelName.
Assert.InRange(count, 0, 1);
count++;
if (count == 1)
{
Assert.Equal("someName", context.ModelName);
}
else
{
Assert.Empty(context.ModelName);
}
})
.Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false)));
var composite = CreateCompositeBinder(modelBinder.Object);
// Act & Assert
var result = await composite.BindModelAsync(bindingContext);
}
[Fact] [Fact]
public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse() public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse()
{ {
@ -322,6 +279,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)),
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext(), OperationBindingContext = new OperationBindingContext(),
ValueProvider = new SimpleValueProvider(),
}; };
// Act // Act
@ -493,6 +451,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
IsTopLevelObject = true,
ModelMetadata = metadataProvider.GetMetadataForType(type), ModelMetadata = metadataProvider.GetMetadataForType(type),
ModelName = "parameter", ModelName = "parameter",
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),

View File

@ -133,7 +133,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal(modelName, result.Key); Assert.Equal(modelName, result.Key);
Assert.NotNull(result.ValidationNode); Assert.NotNull(result.ValidationNode);
@ -171,7 +170,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key); Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode); Assert.NotNull(result.ValidationNode);
@ -223,7 +221,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key); Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode); Assert.NotNull(result.ValidationNode);
@ -265,7 +262,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key); Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode); Assert.NotNull(result.ValidationNode);
@ -300,7 +296,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
Assert.Equal(modelName, result.Key); Assert.Equal(modelName, result.Key);
Assert.NotNull(result.ValidationNode); Assert.NotNull(result.ValidationNode);
@ -310,33 +305,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding() public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
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>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Null(result);
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>(); var binder = new DictionaryModelBinder<string, string>();

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Xunit; using Xunit;
@ -116,6 +117,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(result.Model); Assert.Null(result.Model);
} }
[Theory]
[InlineData(true, "FieldName")]
[InlineData(false, "ModelName")]
public async Task FormFileModelBinder_UsesFieldNameForTopLevelObject(bool isTopLevel, string expected)
{
// Arrange
var formFiles = new FormFileCollection();
formFiles.Add(GetMockFormFile("FieldName", "file1.txt"));
formFiles.Add(GetMockFormFile("ModelName", "file1.txt"));
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
bindingContext.IsTopLevelObject = isTopLevel;
bindingContext.FieldName = "FieldName";
bindingContext.ModelName = "ModelName";
var binder = new FormFileModelBinder();
// Act
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.NotNull(result);
Assert.True(result.IsModelSet);
var file = Assert.IsAssignableFrom<IFormFile>(result.Model);
ContentDispositionHeaderValue contentDisposition;
ContentDispositionHeaderValue.TryParse(file.ContentDisposition, out contentDisposition);
Assert.Equal(expected, contentDisposition.Name);
}
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsNull_WithEmptyContentDisposition() public async Task FormFileModelBinder_ReturnsNull_WithEmptyContentDisposition()
{ {

View File

@ -148,33 +148,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
} }
[Fact] [Fact]
public async Task KeyValuePairModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding() public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
{
// Arrange
var binder = new KeyValuePairModelBinder<string, string>();
var context = CreateContext();
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>));
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Null(result);
}
[Fact]
public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObjectAndNotIsFirstChanceBinding()
{ {
// Arrange // Arrange
var binder = new KeyValuePairModelBinder<string, string>(); var binder = new KeyValuePairModelBinder<string, string>();

View File

@ -46,7 +46,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Same(newModelMetadata.BindingSource, newBindingContext.BindingSource); Assert.Same(newModelMetadata.BindingSource, newBindingContext.BindingSource);
Assert.False(newBindingContext.FallbackToEmptyPrefix); Assert.False(newBindingContext.FallbackToEmptyPrefix);
Assert.Equal("fieldName", newBindingContext.FieldName); Assert.Equal("fieldName", newBindingContext.FieldName);
Assert.False(newBindingContext.IsFirstChanceBinding);
Assert.False(newBindingContext.IsTopLevelObject); Assert.False(newBindingContext.IsTopLevelObject);
Assert.Null(newBindingContext.Model); Assert.Null(newBindingContext.Model);
Assert.Same(newModelMetadata, newBindingContext.ModelMetadata); Assert.Same(newModelMetadata, newBindingContext.ModelMetadata);

View File

@ -20,27 +20,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class MutableObjectModelBinderTest public class MutableObjectModelBinderTest
{ {
[Theory] [Theory]
[InlineData(true, true, "", "", false)] [InlineData(true, true)]
[InlineData(true, false, "", "", true)] [InlineData(false, false)]
[InlineData(false, true, "", "", false)] // !isTopLevelObject && isFirstChanceBinding cases are unexpected public void CanCreateModel_ReturnsTrue_IfIsTopLevelObject(
[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 void CanCreateModel_ReturnsTrue_IfIsTopLevelObjectAndNotIsFirstChanceBinding(
bool isTopLevelObject, bool isTopLevelObject,
bool isFirstChanceBinding,
string binderModelName,
string modelName,
bool expectedCanCreate) bool expectedCanCreate)
{ {
var mockValueProvider = new Mock<IValueProvider>(); var mockValueProvider = new Mock<IValueProvider>();
@ -54,7 +37,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingContext = new ModelBindingContext ModelBindingContext = new ModelBindingContext
{ {
IsTopLevelObject = isTopLevelObject, IsTopLevelObject = isTopLevelObject,
IsFirstChanceBinding = isFirstChanceBinding,
// Random type. // Random type.
ModelMetadata = metadataProvider.GetMetadataForType(typeof(Person)), ModelMetadata = metadataProvider.GetMetadataForType(typeof(Person)),
@ -65,10 +47,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>(), ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
}, },
// CanCreateModel() ignores the BinderModelName and ModelName properties.
BinderModelName = binderModelName,
ModelName = modelName,
}, },
}; };
@ -467,7 +445,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
[Fact] [Fact]
public async Task BindModel_InitsInstance_IfIsTopLevelObjectAndNotIsFirstChanceBinding() public async Task BindModel_InitsInstance_IfIsTopLevelObject()
{ {
// Arrange // Arrange
var mockValueProvider = new Mock<IValueProvider>(); var mockValueProvider = new Mock<IValueProvider>();

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Test
return new TheoryData<ModelBindingResult> return new TheoryData<ModelBindingResult>
{ {
null, null,
new ModelBindingResult("someKey"), // IsFatalError true as well as IsModelSet false. new ModelBindingResult("someKey"), // IsModelSet false.
new ModelBindingResult(model: null, key: "someKey", isModelSet: false), new ModelBindingResult(model: null, key: "someKey", isModelSet: false),
}; };
} }

View File

@ -734,10 +734,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert // Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response); var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(3, json.Count); Assert.Equal(3, json.Count);
// The model prefix 'model' is used in the modelstate keys because the key 'model' is present in the
// query string. This causes modelbinding to commit to using the prefix.
//
// Mono issue - https://github.com/aspnet/External/issues/19 // Mono issue - https://github.com/aspnet/External/issues/19
Assert.Equal(PlatformNormalizer.NormalizeContent("The Field1 field is required."), json["Field1"]); Assert.Equal(PlatformNormalizer.NormalizeContent("The Field1 field is required."), json["model.Field1"]);
Assert.Equal(PlatformNormalizer.NormalizeContent("The Field2 field is required."), json["Field2"]); Assert.Equal(PlatformNormalizer.NormalizeContent("The Field2 field is required."), json["model.Field2"]);
Assert.Equal(PlatformNormalizer.NormalizeContent("The Field3 field is required."), json["Field3"]); Assert.Equal(PlatformNormalizer.NormalizeContent("The Field3 field is required."), json["model.Field3"]);
} }
[Fact] [Fact]

View File

@ -226,7 +226,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// ModelBindingResult // ModelBindingResult
Assert.NotNull(modelBindingResult); Assert.NotNull(modelBindingResult);
Assert.Equal("Parameter1", modelBindingResult.Key); Assert.Equal(string.Empty, modelBindingResult.Key);
// Model // Model
var boundPerson = Assert.IsType<Person>(modelBindingResult.Model); var boundPerson = Assert.IsType<Person>(modelBindingResult.Model);
@ -237,7 +237,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); Assert.Equal(1, modelState.Keys.Count);
var key = Assert.Single(modelState.Keys, k => k == "Parameter1.Address.Street"); var key = Assert.Single(modelState.Keys, k => k == "Address.Street");
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it. Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it.
} }

View File

@ -0,0 +1,256 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Primitives;
using Xunit;
namespace Microsoft.AspNet.Mvc.IntegrationTests
{
// Integration tests for the decision logic about how a model-name/prefix is selected at the top-level
// of ModelBinding.
public class ModelPrefixSelectionIntegrationTest
{
private class Person1
{
[FromForm]
public string Name { get; set; }
}
[Fact]
public async Task ComplexModel_PrefixSelected_ByValueProvider()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person1),
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
// This will cause selection of the "parameter" prefix.
request.QueryString = new QueryString("?parameter=");
// This value won't be used, because we select the "parameter" prefix.
request.Form = new FormCollection(new Dictionary<string, StringValues>()
{
{ "Name", "Billy" },
});
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Equal("parameter", modelBindingResult.Key);
var model = Assert.IsType<Person1>(modelBindingResult.Model);
Assert.Null(model.Name);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private class Person2
{
[FromForm]
public string Name { get; set; }
}
[Fact]
public async Task ComplexModel_PrefixSelected_ByValueProviderValue_WithFilteredValueProviders()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person2),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Query,
},
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
// This will cause selection of the "parameter" prefix.
request.QueryString = new QueryString("?parameter=");
// This value won't be used, because we select the "parameter" prefix.
request.Form = new FormCollection(new Dictionary<string, StringValues>()
{
{ "Name", "Billy" },
});
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Equal("parameter", modelBindingResult.Key);
var model = Assert.IsType<Person2>(modelBindingResult.Model);
Assert.Null(model.Name);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private class Person3
{
[FromForm]
public string Name { get; set; }
}
[Fact]
public async Task ComplexModel_EmptyPrefixSelected_NoMatchingValueProviderValue()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person3),
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
// This can't be used because of [FromForm] on the property.
request.QueryString = new QueryString("?Name=");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Equal(string.Empty, modelBindingResult.Key);
var model = Assert.IsType<Person3>(modelBindingResult.Model);
Assert.Null(model.Name);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private class Person4
{
[FromForm]
public string Name { get; set; }
}
[Fact]
public async Task ComplexModel_EmptyPrefixSelected_NoMatchingValueProviderValue_WithFilteredValueProviders()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person4),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Query,
},
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
// This will only match empty prefix, but can't be used because of [FromForm] on the property.
request.QueryString = new QueryString("?Name=");
// This value won't be used to select a prefix, because we're only looking at the query string.
request.Form = new FormCollection(new Dictionary<string, StringValues>()
{
{ "parameter", string.Empty },
});
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Equal(string.Empty, modelBindingResult.Key);
var model = Assert.IsType<Person4>(modelBindingResult.Model);
Assert.Null(model.Name);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private class Person5
{
[FromForm]
public string Name { get; set; }
}
[Fact]
public async Task ComplexModel_EmptyPrefixSelected_NoMatchingValueProviderValue_WithFilteredValueProviders_NoValues()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person5),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Query,
},
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
// This value won't be used to select a prefix, because we're only looking at the query string.
request.Form = new FormCollection(new Dictionary<string, StringValues>()
{
{ "parameter", string.Empty },
});
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Equal(string.Empty, modelBindingResult.Key);
var model = Assert.IsType<Person5>(modelBindingResult.Model);
Assert.Null(model.Name);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
}
}