Handle broader range of collection types in model binding
- #2793 - add `ICollectionModelBinder`, allowing `GenericModelBinder` to call `CreateEmptyCollection()` - adjust `CollectionModelBinder` and `DictionaryModelBinder` to activate model if default types are incompatible - do not create default (empty) top-level collection in fallback case if Model already non-`null` - change type checks in `GenericModelBinder` to align with `CollectionModelBinder` capabilities - add special case for `IEnumerable<T>` - correct `ModelMetadata` of a few tests that previously did not need that information
This commit is contained in:
parent
b6a109e2a3
commit
d45e2ee3f5
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
|
|
@ -25,15 +27,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return base.BindModelAsync(bindingContext);
|
return base.BindModelAsync(bindingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override object CreateEmptyCollection()
|
/// <inheritdoc />
|
||||||
|
public override bool CanCreateInstance(Type targetType)
|
||||||
{
|
{
|
||||||
|
return targetType == typeof(TElement[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override object CreateEmptyCollection(Type targetType)
|
||||||
|
{
|
||||||
|
Debug.Assert(targetType == typeof(TElement[]), "GenericModelBinder only creates this binder for arrays.");
|
||||||
|
|
||||||
return new TElement[0];
|
return new TElement[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override object GetModel(IEnumerable<TElement> newCollection)
|
protected override object ConvertToCollectionType(Type targetType, IEnumerable<TElement> collection)
|
||||||
{
|
{
|
||||||
return newCollection?.ToArray();
|
Debug.Assert(targetType == typeof(TElement[]), "GenericModelBinder only creates this binder for arrays.");
|
||||||
|
|
||||||
|
// If non-null, collection is a List<TElement>, never already a TElement[].
|
||||||
|
return collection?.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
#if DNXCORE50
|
||||||
|
using System.Reflection;
|
||||||
|
#endif
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
|
|
||||||
|
|
@ -16,22 +19,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
/// <see cref="IModelBinder"/> implementation for binding collection values.
|
/// <see cref="IModelBinder"/> implementation for binding collection values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TElement">Type of elements in the collection.</typeparam>
|
/// <typeparam name="TElement">Type of elements in the collection.</typeparam>
|
||||||
public class CollectionModelBinder<TElement> : IModelBinder
|
public class CollectionModelBinder<TElement> : ICollectionModelBinder
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
|
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
object model;
|
var model = bindingContext.Model;
|
||||||
|
|
||||||
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 for 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 (or use existing Model) and return it.
|
||||||
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
|
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
|
||||||
{
|
{
|
||||||
model = CreateEmptyCollection();
|
if (model == null)
|
||||||
|
{
|
||||||
|
model = CreateEmptyCollection(bindingContext.ModelType);
|
||||||
|
}
|
||||||
|
|
||||||
var validationNode = new ModelValidationNode(
|
var validationNode = new ModelValidationNode(
|
||||||
bindingContext.ModelName,
|
bindingContext.ModelName,
|
||||||
|
|
@ -50,12 +55,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
|
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
|
||||||
|
|
||||||
IEnumerable<TElement> boundCollection;
|
|
||||||
CollectionResult result;
|
CollectionResult result;
|
||||||
if (valueProviderResult == null)
|
if (valueProviderResult == null)
|
||||||
{
|
{
|
||||||
result = await BindComplexCollection(bindingContext);
|
result = await BindComplexCollection(bindingContext);
|
||||||
boundCollection = result.Model;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -63,13 +66,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
bindingContext,
|
bindingContext,
|
||||||
valueProviderResult.RawValue,
|
valueProviderResult.RawValue,
|
||||||
valueProviderResult.Culture);
|
valueProviderResult.Culture);
|
||||||
boundCollection = result.Model;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model = bindingContext.Model;
|
var boundCollection = result.Model;
|
||||||
if (model == null)
|
if (model == null)
|
||||||
{
|
{
|
||||||
model = GetModel(boundCollection);
|
model = ConvertToCollectionType(bindingContext.ModelType, boundCollection);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -84,14 +86,50 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
validationNode: result?.ValidationNode);
|
validationNode: result?.ValidationNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when we're creating a default 'empty' model for a top level bind.
|
/// <inheritdoc />
|
||||||
protected virtual object CreateEmptyCollection()
|
public virtual bool CanCreateInstance(Type targetType)
|
||||||
{
|
{
|
||||||
return new List<TElement>();
|
return CreateEmptyCollection(targetType) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an <see cref="object"/> assignable to <paramref name="targetType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetType"><see cref="Type"/> of the model.</param>
|
||||||
|
/// <returns>An <see cref="object"/> assignable to <paramref name="targetType"/>.</returns>
|
||||||
|
/// <remarks>Called when creating a default 'empty' model for a top level bind.</remarks>
|
||||||
|
protected virtual object CreateEmptyCollection(Type targetType)
|
||||||
|
{
|
||||||
|
if (targetType.IsAssignableFrom(typeof(List<TElement>)))
|
||||||
|
{
|
||||||
|
// Simple case such as ICollection<TElement>, IEnumerable<TElement> and IList<TElement>.
|
||||||
|
return new List<TElement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateInstance(targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an instance of <paramref name="targetType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetType"><see cref="Type"/> of the model.</param>
|
||||||
|
/// <returns>An instance of <paramref name="targetType"/>.</returns>
|
||||||
|
protected object CreateInstance(Type targetType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance(targetType);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Details of exception are not important.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
|
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
|
||||||
// is [ "1", "2" ] and needs to be converted to an int[].
|
// is [ "1", "2" ] and needs to be converted to an int[].
|
||||||
|
// Internal for testing.
|
||||||
internal async Task<CollectionResult> BindSimpleCollection(
|
internal async Task<CollectionResult> BindSimpleCollection(
|
||||||
ModelBindingContext bindingContext,
|
ModelBindingContext bindingContext,
|
||||||
object rawValue,
|
object rawValue,
|
||||||
|
|
@ -156,6 +194,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
|
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal for testing.
|
||||||
internal async Task<CollectionResult> BindComplexCollectionFromIndexes(
|
internal async Task<CollectionResult> BindComplexCollectionFromIndexes(
|
||||||
ModelBindingContext bindingContext,
|
ModelBindingContext bindingContext,
|
||||||
IEnumerable<string> indexNames)
|
IEnumerable<string> indexNames)
|
||||||
|
|
@ -219,6 +258,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal for testing.
|
||||||
internal class CollectionResult
|
internal class CollectionResult
|
||||||
{
|
{
|
||||||
public ModelValidationNode ValidationNode { get; set; }
|
public ModelValidationNode ValidationNode { get; set; }
|
||||||
|
|
@ -227,23 +267,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an <see cref="object"/> assignable to the collection property.
|
/// Gets an <see cref="object"/> assignable to <paramref name="targetType"/> that contains members from
|
||||||
|
/// <paramref name="collection"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="newCollection">
|
/// <param name="targetType"><see cref="Type"/> of the model.</param>
|
||||||
|
/// <param name="collection">
|
||||||
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// <see cref="object"/> assignable to the collection property. Or <c>null</c> if nothing was bound.
|
/// An <see cref="object"/> assignable to <paramref name="targetType"/>. Or <c>null</c> if nothing was bound.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Extensibility point that allows the bound collection to be manipulated or transformed before being
|
/// Extensibility point that allows the bound collection to be manipulated or transformed before being
|
||||||
/// returned from the binder.
|
/// returned from the binder.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
protected virtual object GetModel(IEnumerable<TElement> newCollection)
|
protected virtual object ConvertToCollectionType(Type targetType, IEnumerable<TElement> collection)
|
||||||
{
|
{
|
||||||
// Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List<TElement>
|
if (collection == null)
|
||||||
// instance or null. In addition GenericModelBinder confirms a List<TElement> is assignable to the
|
{
|
||||||
// property prior to instantiating this binder and subclass binders do not call this method.
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType.IsAssignableFrom(typeof(List<TElement>)))
|
||||||
|
{
|
||||||
|
// Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List<TElement>
|
||||||
|
// instance or null.
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCollection = CreateInstance(targetType);
|
||||||
|
CopyToModel(newCollection, collection);
|
||||||
|
|
||||||
return newCollection;
|
return newCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,11 +308,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
/// <param name="sourceCollection">
|
/// <param name="sourceCollection">
|
||||||
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <remarks>Called only in TryUpdateModelAsync(collection, ...) scenarios.</remarks>
|
|
||||||
protected virtual void CopyToModel([NotNull] object target, IEnumerable<TElement> sourceCollection)
|
protected virtual void CopyToModel([NotNull] object target, IEnumerable<TElement> sourceCollection)
|
||||||
{
|
{
|
||||||
var targetCollection = target as ICollection<TElement>;
|
var targetCollection = target as ICollection<TElement>;
|
||||||
Debug.Assert(targetCollection != null); // This binder is instantiated only for ICollection model types.
|
Debug.Assert(targetCollection != null, "This binder is instantiated only for ICollection<T> model types.");
|
||||||
|
|
||||||
if (sourceCollection != null && targetCollection != null && !targetCollection.IsReadOnly)
|
if (sourceCollection != null && targetCollection != null && !targetCollection.IsReadOnly)
|
||||||
{
|
{
|
||||||
|
|
@ -270,7 +323,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static object[] RawValueToObjectArray(object rawValue)
|
private static object[] RawValueToObjectArray(object rawValue)
|
||||||
{
|
{
|
||||||
// precondition: rawValue is not null
|
// precondition: rawValue is not null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
#if DNXCORE50
|
||||||
|
using System.Reflection;
|
||||||
|
#endif
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
|
|
||||||
|
|
@ -27,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Assert(result.Model != null);
|
Debug.Assert(result.Model != null);
|
||||||
var model = (Dictionary<TKey, TValue>)result.Model;
|
var model = (IDictionary<TKey, TValue>)result.Model;
|
||||||
if (model.Count != 0)
|
if (model.Count != 0)
|
||||||
{
|
{
|
||||||
// ICollection<KeyValuePair<TKey, TValue>> approach was successful.
|
// ICollection<KeyValuePair<TKey, TValue>> approach was successful.
|
||||||
|
|
@ -80,15 +84,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override object GetModel(IEnumerable<KeyValuePair<TKey, TValue>> newCollection)
|
protected override object ConvertToCollectionType(
|
||||||
|
Type targetType,
|
||||||
|
IEnumerable<KeyValuePair<TKey, TValue>> collection)
|
||||||
{
|
{
|
||||||
return newCollection?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
if (collection == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType.IsAssignableFrom(typeof(Dictionary<TKey, TValue>)))
|
||||||
|
{
|
||||||
|
// Collection is a List<KeyValuePair<TKey, TValue>>, never already a Dictionary<TKey, TValue>.
|
||||||
|
return collection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCollection = CreateInstance(targetType);
|
||||||
|
CopyToModel(newCollection, collection);
|
||||||
|
|
||||||
|
return newCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override object CreateEmptyCollection()
|
protected override object CreateEmptyCollection(Type targetType)
|
||||||
{
|
{
|
||||||
return new Dictionary<TKey, TValue>();
|
if (targetType.IsAssignableFrom(typeof(Dictionary<TKey, TValue>)))
|
||||||
|
{
|
||||||
|
// Simple case such as IDictionary<TKey, TValue>.
|
||||||
|
return new Dictionary<TKey, TValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateInstance(targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TKey ConvertFromString(string keyString)
|
private static TKey ConvertFromString(string keyString)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
|
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
var binderType = ResolveBinderType(bindingContext.ModelType);
|
var binderType = ResolveBinderType(bindingContext);
|
||||||
if (binderType != null)
|
if (binderType != null)
|
||||||
{
|
{
|
||||||
var binder = (IModelBinder)Activator.CreateInstance(binderType);
|
var binder = (IModelBinder)Activator.CreateInstance(binderType);
|
||||||
var result = await binder.BindModelAsync(bindingContext);
|
|
||||||
|
|
||||||
|
var collectionBinder = binder as ICollectionModelBinder;
|
||||||
|
if (collectionBinder != null &&
|
||||||
|
bindingContext.Model == null &&
|
||||||
|
!collectionBinder.CanCreateInstance(bindingContext.ModelType))
|
||||||
|
{
|
||||||
|
// Able to resolve a binder type but need a new model instance and that binder cannot create it.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await binder.BindModelAsync(bindingContext);
|
||||||
var modelBindingResult = result != null ?
|
var modelBindingResult = result != null ?
|
||||||
result :
|
result :
|
||||||
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
|
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
|
||||||
|
|
@ -31,12 +40,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type ResolveBinderType(Type modelType)
|
private static Type ResolveBinderType(ModelBindingContext context)
|
||||||
{
|
{
|
||||||
|
var modelType = context.ModelType;
|
||||||
|
|
||||||
return GetArrayBinder(modelType) ??
|
return GetArrayBinder(modelType) ??
|
||||||
GetCollectionBinder(modelType) ??
|
GetCollectionBinder(modelType) ??
|
||||||
GetDictionaryBinder(modelType) ??
|
GetDictionaryBinder(modelType) ??
|
||||||
GetKeyValuePairBinder(modelType);
|
GetEnumerableBinder(context) ??
|
||||||
|
GetKeyValuePairBinder(modelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type GetArrayBinder(Type modelType)
|
private static Type GetArrayBinder(Type modelType)
|
||||||
|
|
@ -52,19 +64,50 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
private static Type GetCollectionBinder(Type modelType)
|
private static Type GetCollectionBinder(Type modelType)
|
||||||
{
|
{
|
||||||
return GetGenericBinderType(
|
return GetGenericBinderType(
|
||||||
typeof(ICollection<>),
|
typeof(ICollection<>),
|
||||||
typeof(List<>),
|
typeof(CollectionModelBinder<>),
|
||||||
typeof(CollectionModelBinder<>),
|
modelType);
|
||||||
modelType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type GetDictionaryBinder(Type modelType)
|
private static Type GetDictionaryBinder(Type modelType)
|
||||||
{
|
{
|
||||||
return GetGenericBinderType(
|
return GetGenericBinderType(
|
||||||
typeof(IDictionary<,>),
|
typeof(IDictionary<,>),
|
||||||
typeof(Dictionary<,>),
|
typeof(DictionaryModelBinder<,>),
|
||||||
typeof(DictionaryModelBinder<,>),
|
modelType);
|
||||||
modelType);
|
}
|
||||||
|
|
||||||
|
private static Type GetEnumerableBinder(ModelBindingContext context)
|
||||||
|
{
|
||||||
|
var modelTypeArguments = GetGenericBinderTypeArgs(typeof(IEnumerable<>), context.ModelType);
|
||||||
|
if (modelTypeArguments == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Model == null)
|
||||||
|
{
|
||||||
|
// GetCollectionBinder has already confirmed modelType is not compatible with ICollection<T>. Can a
|
||||||
|
// List<T> (the default CollectionModelBinder type) instance be used instead of that exact type?
|
||||||
|
// Likely this will succeed only if the property type is exactly IEnumerable<T>.
|
||||||
|
var closedListType = typeof(List<>).MakeGenericType(modelTypeArguments);
|
||||||
|
if (!context.ModelType.IsAssignableFrom(closedListType))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A non-null instance must be updated in-place. For that the instance must also implement
|
||||||
|
// ICollection<T>. For example an IEnumerable<T> property may have a List<T> default value.
|
||||||
|
var closedCollectionType = typeof(ICollection<>).MakeGenericType(modelTypeArguments);
|
||||||
|
if (!closedCollectionType.IsAssignableFrom(context.Model.GetType()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof(CollectionModelBinder<>).MakeGenericType(modelTypeArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type GetKeyValuePairBinder(Type modelType)
|
private static Type GetKeyValuePairBinder(Type modelType)
|
||||||
|
|
@ -79,52 +122,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <remarks>
|
// Example: GetGenericBinderType(typeof(IList<T>), typeof(ListBinder<T>), ...) means that the ListBinder<T>
|
||||||
/// Example: <c>GetGenericBinderType(typeof(IList<T>), typeof(List<T>),
|
// type can update models that implement IList<T>. This method will return
|
||||||
/// typeof(ListBinder<T>), ...)</c> means that the <c>ListBinder<T></c> type can update models that
|
// ListBinder<T> or null, depending on whether the type checks succeed.
|
||||||
/// implement <see cref="IList{T}"/>, and if for some reason the existing model instance is not updatable the
|
private static Type GetGenericBinderType(Type supportedInterfaceType, Type openBinderType, Type modelType)
|
||||||
/// binder will create a <see cref="List{T}"/> object and bind to that instead. This method will return
|
|
||||||
/// <c>ListBinder<T></c> or <c>null</c>, depending on whether the type and updatability checks succeed.
|
|
||||||
/// </remarks>
|
|
||||||
private static Type GetGenericBinderType(Type supportedInterfaceType,
|
|
||||||
Type newInstanceType,
|
|
||||||
Type openBinderType,
|
|
||||||
Type modelType)
|
|
||||||
{
|
{
|
||||||
Debug.Assert(supportedInterfaceType != null);
|
|
||||||
Debug.Assert(openBinderType != null);
|
Debug.Assert(openBinderType != null);
|
||||||
Debug.Assert(modelType != null);
|
|
||||||
|
|
||||||
var modelTypeArguments = GetGenericBinderTypeArgs(supportedInterfaceType, modelType);
|
var modelTypeArguments = GetGenericBinderTypeArgs(supportedInterfaceType, modelType);
|
||||||
|
|
||||||
if (modelTypeArguments == null)
|
if (modelTypeArguments == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var closedNewInstanceType = newInstanceType.MakeGenericType(modelTypeArguments);
|
|
||||||
if (!modelType.GetTypeInfo().IsAssignableFrom(closedNewInstanceType.GetTypeInfo()))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return openBinderType.MakeGenericType(modelTypeArguments);
|
return openBinderType.MakeGenericType(modelTypeArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the generic arguments for the binder, based on the model type. Or null if not compatible.
|
// Get the generic arguments for the binder, based on the model type. Or null if not compatible.
|
||||||
private static Type[] GetGenericBinderTypeArgs(Type supportedInterfaceType, Type modelType)
|
private static Type[] GetGenericBinderTypeArgs(Type supportedInterfaceType, Type modelType)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(supportedInterfaceType != null);
|
||||||
|
Debug.Assert(modelType != null);
|
||||||
|
|
||||||
var modelTypeInfo = modelType.GetTypeInfo();
|
var modelTypeInfo = modelType.GetTypeInfo();
|
||||||
if (!modelTypeInfo.IsGenericType || modelTypeInfo.IsGenericTypeDefinition)
|
if (!modelTypeInfo.IsGenericType || modelTypeInfo.IsGenericTypeDefinition)
|
||||||
{
|
{
|
||||||
// not a closed generic type
|
// modelType is not a closed generic type.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var modelTypeArguments = modelTypeInfo.GenericTypeArguments;
|
var modelTypeArguments = modelTypeInfo.GenericTypeArguments;
|
||||||
if (modelTypeArguments.Length != supportedInterfaceType.GetTypeInfo().GenericTypeParameters.Length)
|
if (modelTypeArguments.Length != supportedInterfaceType.GetTypeInfo().GenericTypeParameters.Length)
|
||||||
{
|
{
|
||||||
// wrong number of generic type arguments
|
// Wrong number of generic type arguments.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var closedInstanceType = supportedInterfaceType.MakeGenericType(modelTypeArguments);
|
||||||
|
if (!closedInstanceType.IsAssignableFrom(modelType))
|
||||||
|
{
|
||||||
|
// modelType is not compatible with supportedInterfaceType.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for model binding collections.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICollectionModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an indication whether or not this <see cref="ICollectionModelBinder"/> implementation can create
|
||||||
|
/// an <see cref="object"/> assignable to <paramref name="targetType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetType"><see cref="Type"/> of the model.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if this <see cref="ICollectionModelBinder"/> implementation can create an <see cref="object"/>
|
||||||
|
/// assignable to <paramref name="targetType"/>; <c>false</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// A <c>true</c> return value is necessary for successful model binding if model is initially <c>null</c>.
|
||||||
|
/// </remarks>
|
||||||
|
bool CanCreateInstance(Type targetType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
#if DNX451
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
#if DNX451
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -275,6 +276,44 @@ 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
|
||||||
|
// Model already has a value.
|
||||||
|
[Fact]
|
||||||
|
public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var binder = new CollectionModelBinder<string>();
|
||||||
|
|
||||||
|
var context = CreateContext();
|
||||||
|
context.IsTopLevelObject = true;
|
||||||
|
|
||||||
|
var list = new List<string>();
|
||||||
|
context.Model = list;
|
||||||
|
|
||||||
|
// 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>));
|
||||||
|
|
||||||
|
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
|
||||||
|
Assert.Same(list, result.Model);
|
||||||
|
Assert.Empty(list);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData("param")]
|
[InlineData("param")]
|
||||||
|
|
@ -300,6 +339,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model type -> can create instance.
|
||||||
|
public static TheoryData<Type, bool> CanCreateInstanceData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<Type, bool>
|
||||||
|
{
|
||||||
|
{ typeof(IEnumerable<int>), true },
|
||||||
|
{ typeof(ICollection<int>), true },
|
||||||
|
{ typeof(IList<int>), true },
|
||||||
|
{ typeof(List<int>), true },
|
||||||
|
{ typeof(LinkedList<int>), true },
|
||||||
|
{ typeof(ISet<int>), false },
|
||||||
|
{ typeof(ListWithInternalConstructor<int>), false },
|
||||||
|
{ typeof(ListWithThrowingConstructor<int>), false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(CanCreateInstanceData))]
|
||||||
|
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = binder.CanCreateInstance(modelType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
#if DNX451
|
#if DNX451
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindSimpleCollection_SubBindingSucceeds()
|
public async Task BindSimpleCollection_SubBindingSucceeds()
|
||||||
|
|
@ -335,7 +407,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var bindingContext = new ModelBindingContext
|
var bindingContext = new ModelBindingContext
|
||||||
{
|
{
|
||||||
ModelMetadata = metadataProvider.GetMetadataForType(typeof(int)),
|
ModelMetadata = metadataProvider.GetMetadataForType(typeof(IList<int>)),
|
||||||
ModelName = "someName",
|
ModelName = "someName",
|
||||||
ValueProvider = valueProvider,
|
ValueProvider = valueProvider,
|
||||||
OperationBindingContext = new OperationBindingContext
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
|
@ -387,5 +459,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
{
|
{
|
||||||
public List<string> ListProperty { get; set; }
|
public List<string> ListProperty { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModelWithSimpleProperties
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ListWithInternalConstructor<T> : List<T>
|
||||||
|
{
|
||||||
|
internal ListWithInternalConstructor()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ListWithThrowingConstructor<T> : List<T>
|
||||||
|
{
|
||||||
|
public ListWithThrowingConstructor()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
throw new ApplicationException("No, don't do this.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithDictionaryProperty),
|
typeof(ModelWithDictionaryProperties),
|
||||||
nameof(ModelWithDictionaryProperty.DictionaryProperty));
|
nameof(ModelWithDictionaryProperties.DictionaryProperty));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await binder.BindModelAsync(context);
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
@ -150,8 +150,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithDictionaryProperty),
|
typeof(ModelWithDictionaryProperties),
|
||||||
nameof(ModelWithDictionaryProperty.DictionaryProperty));
|
nameof(ModelWithDictionaryProperties.DictionaryProperty));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await binder.BindModelAsync(context);
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
@ -202,8 +202,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithDictionaryProperty),
|
typeof(ModelWithDictionaryProperties),
|
||||||
nameof(ModelWithDictionaryProperty.DictionaryProperty));
|
nameof(ModelWithDictionaryProperties.DictionaryWithValueTypesProperty));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await binder.BindModelAsync(context);
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
@ -244,8 +244,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithDictionaryProperty),
|
typeof(ModelWithDictionaryProperties),
|
||||||
nameof(ModelWithDictionaryProperty.DictionaryProperty));
|
nameof(ModelWithDictionaryProperties.DictionaryWithComplexValuesProperty));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await binder.BindModelAsync(context);
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
@ -261,6 +261,41 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
Assert.Equal(dictionary, resultDictionary);
|
Assert.Equal(dictionary, resultDictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(StringToStringData))]
|
||||||
|
public async Task BindModel_FallsBackToBindingValues_WithCustomDictionary(
|
||||||
|
string modelName,
|
||||||
|
string keyFormat,
|
||||||
|
IDictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedDictionary = new SortedDictionary<string, string>(dictionary);
|
||||||
|
var binder = new DictionaryModelBinder<string, string>();
|
||||||
|
var context = CreateContext();
|
||||||
|
context.ModelName = modelName;
|
||||||
|
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
|
||||||
|
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
|
||||||
|
context.ValueProvider = context.OperationBindingContext.ValueProvider;
|
||||||
|
|
||||||
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
|
typeof(ModelWithDictionaryProperties),
|
||||||
|
nameof(ModelWithDictionaryProperties.CustomDictionaryProperty));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await binder.BindModelAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.False(result.IsFatalError);
|
||||||
|
Assert.True(result.IsModelSet);
|
||||||
|
Assert.Equal(modelName, result.Key);
|
||||||
|
Assert.NotNull(result.ValidationNode);
|
||||||
|
|
||||||
|
var resultDictionary = Assert.IsAssignableFrom<SortedDictionary<string, string>>(result.Model);
|
||||||
|
Assert.Equal(expectedDictionary, resultDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
|
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
|
||||||
{
|
{
|
||||||
|
|
@ -332,8 +367,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
|
||||||
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
var metadataProvider = context.OperationBindingContext.MetadataProvider;
|
||||||
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
|
||||||
typeof(ModelWithDictionaryProperty),
|
typeof(ModelWithDictionaryProperties),
|
||||||
nameof(ModelWithDictionaryProperty.DictionaryProperty));
|
nameof(ModelWithDictionaryProperties.DictionaryProperty));
|
||||||
|
|
||||||
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
|
context.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
|
||||||
|
|
||||||
|
|
@ -344,6 +379,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model type -> can create instance.
|
||||||
|
public static TheoryData<Type, bool> CanCreateInstanceData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<Type, bool>
|
||||||
|
{
|
||||||
|
{ typeof(IEnumerable<KeyValuePair<int, int>>), true },
|
||||||
|
{ typeof(ICollection<KeyValuePair<int, int>>), true },
|
||||||
|
{ typeof(IDictionary<int, int>), true },
|
||||||
|
{ typeof(Dictionary<int, int>), true },
|
||||||
|
{ typeof(SortedDictionary<int, int>), true },
|
||||||
|
{ typeof(IList<KeyValuePair<int, int>>), false },
|
||||||
|
{ typeof(DictionaryWithInternalConstructor<int, int>), false },
|
||||||
|
{ typeof(DictionaryWithThrowingConstructor<int, int>), false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(CanCreateInstanceData))]
|
||||||
|
public void CanCreateInstance_ReturnsExpectedValue(Type modelType, bool expectedResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var binder = new DictionaryModelBinder<int, int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = binder.CanCreateInstance(modelType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
private static ModelBindingContext CreateContext()
|
private static ModelBindingContext CreateContext()
|
||||||
{
|
{
|
||||||
var modelBindingContext = new ModelBindingContext()
|
var modelBindingContext = new ModelBindingContext()
|
||||||
|
|
@ -401,7 +469,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
private static ModelBindingContext GetModelBindingContext(bool isReadOnly)
|
private static ModelBindingContext GetModelBindingContext(bool isReadOnly)
|
||||||
{
|
{
|
||||||
var metadataProvider = new TestModelMetadataProvider();
|
var metadataProvider = new TestModelMetadataProvider();
|
||||||
metadataProvider.ForType<List<int>>().BindingDetails(bd => bd.IsReadOnly = isReadOnly);
|
metadataProvider.ForType<IDictionary<int, string>>().BindingDetails(bd => bd.IsReadOnly = isReadOnly);
|
||||||
var valueProvider = new SimpleHttpValueProvider
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
{
|
{
|
||||||
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
|
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
|
||||||
|
|
@ -441,9 +509,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
return mockKvpBinder.Object;
|
return mockKvpBinder.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ModelWithDictionaryProperty
|
private class ModelWithDictionaryProperties
|
||||||
{
|
{
|
||||||
|
// A Dictionary<string, string> instance cannot be assigned to this property.
|
||||||
|
public SortedDictionary<string, string> CustomDictionaryProperty { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, string> DictionaryProperty { get; set; }
|
public Dictionary<string, string> DictionaryProperty { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<int, ModelWithProperties> DictionaryWithComplexValuesProperty { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<long, int> DictionaryWithValueTypesProperty { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ModelWithProperties
|
private class ModelWithProperties
|
||||||
|
|
@ -471,6 +546,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
return $"{{{ Id }, '{ Name }'}}";
|
return $"{{{ Id }, '{ Name }'}}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class DictionaryWithInternalConstructor<TKey, TValue> : Dictionary<TKey, TValue>
|
||||||
|
{
|
||||||
|
internal DictionaryWithInternalConstructor()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DictionaryWithThrowingConstructor<TKey, TValue> : Dictionary<TKey, TValue>
|
||||||
|
{
|
||||||
|
public DictionaryWithThrowingConstructor()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
throw new ApplicationException("No, don't do this.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var url =
|
var url = "http://localhost/FromQueryAttribute_Company/CreateDepartment?TestEmployees[0].EmployeeId=1234";
|
||||||
"http://localhost/FromQueryAttribute_Company/CreateDepartment?TestEmployees[0].EmployeeId=1234";
|
|
||||||
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.GetAsync(url);
|
var response = await client.GetAsync(url);
|
||||||
|
|
@ -118,7 +116,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var url =
|
var url =
|
||||||
"http://localhost/FromQueryAttribute_Company/ValidateDepartment?TestEmployees[0].Department=contoso";
|
"http://localhost/FromQueryAttribute_Company/ValidateDepartment?TestEmployees[0].Department=contoso";
|
||||||
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.GetAsync(url);
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
public CustomReadOnlyCollection<Address> Address { get; set; }
|
public CustomReadOnlyCollection<Address> Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Concrete Collection types don't work with GenericModelBinder #2793")]
|
[Fact]
|
||||||
public async Task ActionParameter_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound()
|
public async Task ActionParameter_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -106,12 +106,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.NotNull(boundModel);
|
Assert.NotNull(boundModel);
|
||||||
Assert.NotNull(boundModel.Address);
|
Assert.NotNull(boundModel.Address);
|
||||||
|
|
||||||
// Arrays should not be updated.
|
// Read-only collection should not be updated.
|
||||||
Assert.Equal(0, boundModel.Address.Count());
|
Assert.Empty(boundModel.Address);
|
||||||
|
|
||||||
// ModelState
|
// ModelState (data is valid but is not copied into Address).
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
Assert.Empty(modelState.Keys);
|
var entry = Assert.Single(modelState);
|
||||||
|
Assert.Equal("Address[0].Street", entry.Key);
|
||||||
|
var state = entry.Value;
|
||||||
|
Assert.NotNull(state);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
|
||||||
|
Assert.Equal("SomeStreet", state.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Person4
|
private class Person4
|
||||||
|
|
@ -252,7 +257,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Concrete Collection types don't work with GenericModelBinder #2793")]
|
[Fact]
|
||||||
public async Task ActionParameter_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound()
|
public async Task ActionParameter_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -285,12 +290,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.NotNull(boundModel);
|
Assert.NotNull(boundModel);
|
||||||
Assert.NotNull(boundModel.Address);
|
Assert.NotNull(boundModel.Address);
|
||||||
|
|
||||||
// Arrays should not be updated.
|
// Read-only collection should not be updated.
|
||||||
Assert.Equal(0, boundModel.Address.Count());
|
Assert.Empty(boundModel.Address);
|
||||||
|
|
||||||
// ModelState
|
// ModelState (data is valid but is not copied into Address).
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
Assert.Empty(modelState.Keys);
|
var entry = Assert.Single(modelState);
|
||||||
|
Assert.Equal("prefix.Address[0].Street", entry.Key);
|
||||||
|
var state = entry.Value;
|
||||||
|
Assert.NotNull(state);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
|
||||||
|
Assert.Equal("SomeStreet", state.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -380,11 +390,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.Empty(modelState.Keys);
|
Assert.Empty(modelState.Keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CustomReadOnlyCollection<T> : ICollection<T>, IReadOnlyCollection<T>
|
private class CustomReadOnlyCollection<T> : ICollection<T>
|
||||||
{
|
{
|
||||||
private ICollection<T> _original;
|
private ICollection<T> _original;
|
||||||
|
|
||||||
public CustomReadOnlyCollection() : this(new List<T>())
|
public CustomReadOnlyCollection()
|
||||||
|
: this(new List<T>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
public CustomReadOnlyCollection<Address> Address { get; set; }
|
public CustomReadOnlyCollection<Address> Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Concrete Collection types don't work with GenericModelBinder #2793")]
|
[Fact(Skip = "Validation incorrect for collections when using TryUpdateModel, #2941")]
|
||||||
public async Task TryUpdateModel_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound()
|
public async Task TryUpdateModel_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -184,6 +184,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
|
|
||||||
var modelState = new ModelStateDictionary();
|
var modelState = new ModelStateDictionary();
|
||||||
var model = new Person6();
|
var model = new Person6();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await TryUpdateModel(model, string.Empty, operationContext, modelState);
|
var result = await TryUpdateModel(model, string.Empty, operationContext, modelState);
|
||||||
|
|
||||||
|
|
@ -193,12 +194,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
// Model
|
// Model
|
||||||
Assert.NotNull(model.Address);
|
Assert.NotNull(model.Address);
|
||||||
|
|
||||||
// Arrays should not be updated.
|
// Read-only collection should not be updated.
|
||||||
Assert.Equal(0, model.Address.Count());
|
Assert.Empty(model.Address);
|
||||||
|
|
||||||
// ModelState
|
// ModelState (data is valid but is not copied into Address).
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
Assert.Empty(modelState.Keys);
|
var entry = Assert.Single(modelState);
|
||||||
|
Assert.Equal("Address[0].Street", entry.Key);
|
||||||
|
var state = entry.Value;
|
||||||
|
Assert.NotNull(state);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
|
||||||
|
Assert.Equal("SomeStreet", state.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Person4
|
private class Person4
|
||||||
|
|
@ -409,7 +415,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Concrete Collection types don't work with GenericModelBinder #2793")]
|
[Fact(Skip = "Validation incorrect for collections when using TryUpdateModel, #2941")]
|
||||||
public async Task TryUpdateModel_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound()
|
public async Task TryUpdateModel_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -420,6 +426,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
|
|
||||||
var modelState = new ModelStateDictionary();
|
var modelState = new ModelStateDictionary();
|
||||||
var model = new Person6();
|
var model = new Person6();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await TryUpdateModel(model, "prefix", operationContext, modelState);
|
var result = await TryUpdateModel(model, "prefix", operationContext, modelState);
|
||||||
|
|
||||||
|
|
@ -429,12 +436,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
// Model
|
// Model
|
||||||
Assert.NotNull(model.Address);
|
Assert.NotNull(model.Address);
|
||||||
|
|
||||||
// Arrays should not be updated.
|
// Read-only collection should not be updated.
|
||||||
Assert.Equal(0, model.Address.Count());
|
Assert.Empty(model.Address);
|
||||||
|
|
||||||
// ModelState
|
// ModelState (data is valid but is not copied into Address).
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
Assert.Empty(modelState.Keys);
|
var entry = Assert.Single(modelState);
|
||||||
|
Assert.Equal("prefix.Address[0].Street", entry.Key);
|
||||||
|
var state = entry.Value;
|
||||||
|
Assert.NotNull(state);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
|
||||||
|
Assert.Equal("SomeStreet", state.Value.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -499,11 +511,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
||||||
Assert.Empty(modelState.Keys);
|
Assert.Empty(modelState.Keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CustomReadOnlyCollection<T> : ICollection<T>, IReadOnlyCollection<T>
|
private class CustomReadOnlyCollection<T> : ICollection<T>
|
||||||
{
|
{
|
||||||
private ICollection<T> _original;
|
private ICollection<T> _original;
|
||||||
|
|
||||||
public CustomReadOnlyCollection() : this(new List<T>())
|
public CustomReadOnlyCollection()
|
||||||
|
: this(new List<T>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue