// 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().ToArray(); } // fallback return new[] { rawValue }; } private static IEnumerable GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResult) { IEnumerable indexNames = null; if (valueProviderResult != null) { var indexes = (string[])valueProviderResult; if (indexes != null && indexes.Length > 0) { indexNames = indexes; } } return indexNames; } } }