Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Handle broader range of collection types in model binding #2946

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/Microsoft.AspNet.Mvc.Core/ModelBinding/ArrayModelBinder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
Expand All @@ -25,15 +27,29 @@ public override Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingCo
return base.BindModelAsync(bindingContext);
}

protected override object CreateEmptyCollection()
/// <inheritdoc />
public override bool CanCreateInstance(Type targetType)
{
Debug.Assert(targetType == typeof(TElement[]), "GenericModelBinder only creates this binder for arrays.");

return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why just do the thing you put in the assert? You could say with like 20 characters what all of this does, and technically be more correct 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

/// <inheritdoc />
protected override object CreateEmptyCollection(Type targetType)
{
Debug.Assert(targetType == typeof(TElement[]), "GenericModelBinder only creates this binder for arrays.");

return new TElement[0];
}

/// <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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the default case here be that the model is already an array? What are the cases we might get here and have it not be an array?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll never be an array since the base CollectionModelBinder uses a List<TElement> until it needs to copy everything into the final model. The value should not be null but this was written defensively originally...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Can you add that info to a comment here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

}

/// <inheritdoc />
Expand Down
99 changes: 76 additions & 23 deletions src/Microsoft.AspNet.Mvc.Core/ModelBinding/CollectionModelBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using System.Diagnostics;
using System.Globalization;
using System.Linq;
#if DNXCORE50
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.Framework.Internal;

Expand All @@ -16,22 +19,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <see cref="IModelBinder"/> implementation for binding collection values.
/// </summary>
/// <typeparam name="TElement">Type of elements in the collection.</typeparam>
public class CollectionModelBinder<TElement> : IModelBinder
public class CollectionModelBinder<TElement> : ICollectionModelBinder
{
/// <inheritdoc />
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);

object model;

var model = bindingContext.Model;
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
// default 'empty' model and return it.
// default 'empty' model (or use existing Model) and return it.
if (!bindingContext.IsFirstChanceBinding && bindingContext.IsTopLevelObject)
{
model = CreateEmptyCollection();
if (model == null)
{
model = CreateEmptyCollection(bindingContext.ModelType);
}

var validationNode = new ModelValidationNode(
bindingContext.ModelName,
Expand All @@ -50,26 +55,23 @@ public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBind

var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);

IEnumerable<TElement> boundCollection;
CollectionResult result;
if (valueProviderResult == null)
{
result = await BindComplexCollection(bindingContext);
boundCollection = result.Model;
}
else
{
result = await BindSimpleCollection(
bindingContext,
valueProviderResult.RawValue,
valueProviderResult.Culture);
boundCollection = result.Model;
}

model = bindingContext.Model;
var boundCollection = result.Model;
if (model == null)
{
model = GetModel(boundCollection);
model = ConvertToCollectionType(bindingContext.ModelType, boundCollection);
}
else
{
Expand All @@ -84,14 +86,50 @@ public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBind
validationNode: result?.ValidationNode);
}

// Called when we're creating a default 'empty' model for a top level bind.
protected virtual object CreateEmptyCollection()
/// <inheritdoc />
public virtual bool CanCreateInstance(Type targetType)
{
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)
{
return new List<TElement>();
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
// is [ "1", "2" ] and needs to be converted to an int[].
// Internal for testing.
internal async Task<CollectionResult> BindSimpleCollection(
ModelBindingContext bindingContext,
object rawValue,
Expand Down Expand Up @@ -156,6 +194,7 @@ private async Task<CollectionResult> BindComplexCollection(ModelBindingContext b
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
}

// Internal for testing.
internal async Task<CollectionResult> BindComplexCollectionFromIndexes(
ModelBindingContext bindingContext,
IEnumerable<string> indexNames)
Expand Down Expand Up @@ -219,6 +258,7 @@ internal async Task<CollectionResult> BindComplexCollectionFromIndexes(
};
}

// Internal for testing.
internal class CollectionResult
{
public ModelValidationNode ValidationNode { get; set; }
Expand All @@ -227,23 +267,37 @@ internal class CollectionResult
}

/// <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>
/// <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.
/// </param>
/// <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>
/// <remarks>
/// Extensibility point that allows the bound collection to be manipulated or transformed before being
/// returned from the binder.
/// </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>
// 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.
if (collection == null)
{
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;
}

Expand All @@ -254,11 +308,10 @@ protected virtual object GetModel(IEnumerable<TElement> newCollection)
/// <param name="sourceCollection">
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
/// </param>
/// <remarks>Called only in TryUpdateModelAsync(collection, ...) scenarios.</remarks>
protected virtual void CopyToModel([NotNull] object target, IEnumerable<TElement> sourceCollection)
{
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)
{
Expand All @@ -270,7 +323,7 @@ protected virtual void CopyToModel([NotNull] object target, IEnumerable<TElement
}
}

internal static object[] RawValueToObjectArray(object rawValue)
private static object[] RawValueToObjectArray(object rawValue)
{
// precondition: rawValue is not null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
#if DNXCORE50
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.Framework.Internal;

Expand All @@ -27,7 +31,7 @@ public override async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBin
}

Debug.Assert(result.Model != null);
var model = (Dictionary<TKey, TValue>)result.Model;
var model = (IDictionary<TKey, TValue>)result.Model;
if (model.Count != 0)
{
// ICollection<KeyValuePair<TKey, TValue>> approach was successful.
Expand Down Expand Up @@ -80,15 +84,37 @@ public override async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBin
}

/// <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>)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will collection be here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A List<KeyValuePair<TKey, TValue>>. Are you asking for a comment stating that it's never already a dictionary? Note the parameter is used only as it's declared -- IEnumerable<KeyValuePair<TKey, TValue>>.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a comment would make it must more clear 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this iteration includes a comment I added 2 lines below.

{
// 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 />
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)
Expand Down
Loading