// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
#if DOTNET5_4
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
///
/// implementation for binding collection values.
///
/// Type of elements in the collection.
public class CollectionModelBinder : ICollectionModelBinder
{
///
public virtual async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
ModelBindingHelper.ValidateBindingContext(bindingContext);
var model = bindingContext.Model;
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
{
// If we failed to find data for a top-level model, then generate a
// default 'empty' model (or use existing Model) and return it.
if (bindingContext.IsTopLevelObject)
{
if (model == null)
{
model = CreateEmptyCollection(bindingContext.ModelType);
}
return ModelBindingResult.Success(bindingContext.ModelName, model);
}
return ModelBindingResult.NoResult;
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
CollectionResult result;
if (valueProviderResult == ValueProviderResult.None)
{
result = await BindComplexCollection(bindingContext);
}
else
{
result = await BindSimpleCollection(bindingContext, valueProviderResult);
}
var boundCollection = result.Model;
if (model == null)
{
model = ConvertToCollectionType(bindingContext.ModelType, boundCollection);
}
else
{
// Special case for TryUpdateModelAsync(collection, ...) scenarios. Model is null in all other cases.
CopyToModel(model, boundCollection);
}
Debug.Assert(model != null);
if (result.ValidationStrategy != null)
{
bindingContext.ValidationState.Add(model, new ValidationStateEntry()
{
Strategy = result.ValidationStrategy,
});
}
if (valueProviderResult != ValueProviderResult.None)
{
// If we did simple binding, then modelstate should be updated to reflect what we bound for ModelName.
// If we did complex binding, there will already be an entry for each index.
bindingContext.ModelState.SetModelValue(
bindingContext.ModelName,
valueProviderResult);
}
return ModelBindingResult.Success(bindingContext.ModelName, model);
}
///
public virtual bool CanCreateInstance(Type targetType)
{
return CreateEmptyCollection(targetType) != null;
}
///
/// Create an assignable to .
///
/// of the model.
/// An assignable to .
/// Called when creating a default 'empty' model for a top level bind.
protected virtual object CreateEmptyCollection(Type targetType)
{
if (targetType.IsAssignableFrom(typeof(List)))
{
// Simple case such as ICollection, IEnumerable and IList.
return new List();
}
return CreateInstance(targetType);
}
///
/// Create an instance of .
///
/// of the model.
/// An instance of .
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
// is [ "1", "2" ] and needs to be converted to an int[].
// Internal for testing.
internal async Task BindSimpleCollection(
ModelBindingContext bindingContext,
ValueProviderResult values)
{
var boundCollection = new List();
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement));
var innerBindingContext = ModelBindingContext.CreateChildBindingContext(
bindingContext,
elementMetadata,
fieldName: bindingContext.FieldName,
modelName: bindingContext.ModelName,
model: null);
foreach (var value in values)
{
innerBindingContext.ValueProvider = new CompositeValueProvider
{
// our temporary provider goes at the front of the list
new ElementalValueProvider(bindingContext.ModelName, value, values.Culture),
bindingContext.ValueProvider
};
object boundValue = null;
var result =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
if (result != null && result.IsModelSet)
{
boundValue = result.Model;
boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue));
}
}
return new CollectionResult
{
Model = boundCollection
};
}
// Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
private Task BindComplexCollection(ModelBindingContext bindingContext)
{
var indexPropertyName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "index");
var valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName);
var indexNames = GetIndexNamesFromValueProviderResult(valueProviderResultIndex);
return BindComplexCollectionFromIndexes(bindingContext, indexNames);
}
// Internal for testing.
internal async Task BindComplexCollectionFromIndexes(
ModelBindingContext bindingContext,
IEnumerable indexNames)
{
bool indexNamesIsFinite;
if (indexNames != null)
{
indexNamesIsFinite = true;
}
else
{
indexNamesIsFinite = false;
indexNames = Enumerable.Range(0, int.MaxValue)
.Select(i => i.ToString(CultureInfo.InvariantCulture));
}
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement));
var boundCollection = new List();
foreach (var indexName in indexNames)
{
var fullChildName = ModelNames.CreateIndexModelName(bindingContext.ModelName, indexName);
var childBindingContext = ModelBindingContext.CreateChildBindingContext(
bindingContext,
elementMetadata,
fieldName: indexName,
modelName: fullChildName,
model: null);
var didBind = false;
object boundValue = null;
var result =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
if (result != null && result.IsModelSet)
{
didBind = true;
boundValue = result.Model;
}
// infinite size collection stops on first bind failure
if (!didBind && !indexNamesIsFinite)
{
break;
}
boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue));
}
return new CollectionResult
{
Model = boundCollection,
// If we're working with a fixed set of indexes then this is the format like:
//
// ?parameter.index=zero,one,two¶meter[zero]=0&¶meter[one]=1¶meter[two]=2...
//
// We need to provide this data to the validation system so it can 'replay' the keys.
// But we can't just set ValidationState here, because it needs the 'real' model.
ValidationStrategy = indexNamesIsFinite ?
new ExplicitIndexCollectionValidationStrategy(indexNames) :
null,
};
}
// Internal for testing.
internal class CollectionResult
{
public IEnumerable Model { get; set; }
public IValidationStrategy ValidationStrategy { get; set; }
}
///
/// Gets an assignable to that contains members from
/// .
///
/// of the model.
///
/// Collection of values retrieved from value providers. Or null if nothing was bound.
///
///
/// An assignable to . Or null if nothing was bound.
///
///
/// Extensibility point that allows the bound collection to be manipulated or transformed before being
/// returned from the binder.
///
protected virtual object ConvertToCollectionType(Type targetType, IEnumerable collection)
{
if (collection == null)
{
return null;
}
if (targetType.IsAssignableFrom(typeof(List)))
{
// Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List
// instance or null.
return collection;
}
var newCollection = CreateInstance(targetType);
CopyToModel(newCollection, collection);
return newCollection;
}
///
/// Adds values from to given .
///
/// into which values are copied.
///
/// Collection of values retrieved from value providers. Or null if nothing was bound.
///
protected virtual void CopyToModel(object target, IEnumerable sourceCollection)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
var targetCollection = target as ICollection;
Debug.Assert(targetCollection != null, "This binder is instantiated only for ICollection model types.");
if (sourceCollection != null && targetCollection != null && !targetCollection.IsReadOnly)
{
targetCollection.Clear();
foreach (var element in sourceCollection)
{
targetCollection.Add(element);
}
}
}
private static object[] RawValueToObjectArray(object rawValue)
{
// precondition: rawValue is not null
// Need to special-case String so it's not caught by the IEnumerable check which follows
if (rawValue is string)
{
return new[] { rawValue };
}
var rawValueAsObjectArray = rawValue as object[];
if (rawValueAsObjectArray != null)
{
return rawValueAsObjectArray;
}
var rawValueAsEnumerable = rawValue as IEnumerable;
if (rawValueAsEnumerable != null)
{
return rawValueAsEnumerable.Cast