// 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.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Mvc.IntegrationTests { // Integration tests targeting the behavior of the CollectionModelBinder with other model binders. // // Note that CollectionModelBinder handles both ICollection{T} and IList{T} public class CollectionModelBinderIntegrationTest { [Fact] public async Task CollectionModelBinder_BindsListOfSimpleType_WithPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter[0]=10¶meter[1]=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new List() { 10, 11 }, model); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0]").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1]").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); } [Theory] [InlineData("?prefix[0]=10&prefix[1]=11")] [InlineData("?prefix.index=low&prefix.index=high&prefix[low]=10&prefix[high]=11")] [InlineData("?prefix.index=index&prefix.index=indexer&prefix[index]=10&prefix[indexer]=11")] [InlineData("?prefix.index=index&prefix.index=indexer&prefix[index]=10&prefix[indexer]=11&prefix[extra]=12")] public async Task CollectionModelBinder_BindsListOfSimpleType_WithExplicitPrefix_Success(string queryString) { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", BindingInfo = new BindingInfo() { BinderModelName = "prefix", }, ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new List() { 10, 11 }, model); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Theory] [InlineData("?[0]=10&[1]=11")] [InlineData("?index=low&index=high&[high]=11&[low]=10")] [InlineData("?index=index&index=indexer&[indexer]=11&[index]=10")] [InlineData("?index=index&index=indexer&[indexer]=11&[index]=10&[extra]=12")] public async Task CollectionModelBinder_BindsCollectionOfSimpleType_EmptyPrefix_Success(string queryString) { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new List { 10, 11 }, model); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Fact] public async Task CollectionModelBinder_BindsListOfSimpleType_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.Empty(Assert.IsType>(modelBindingResult.Model)); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } private class Person { public int Id { get; set; } } [Theory] [InlineData("?[0].Id=10&[1].Id=11")] [InlineData("?index=low&index=high&[low].Id=10&[high].Id=11")] [InlineData("?parameter[0].Id=10¶meter[1].Id=11")] [InlineData("?parameter.index=low¶meter.index=high¶meter[low].Id=10¶meter[high].Id=11")] [InlineData("?parameter.index=index¶meter.index=indexer¶meter[index].Id=10¶meter[indexer].Id=11")] public async Task CollectionModelBinder_BindsListOfComplexType_ImpliedPrefix_Success(string queryString) { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Equal(11, model[1].Id); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Theory] [InlineData("?prefix[0].Id=10&prefix[1].Id=11")] [InlineData("?prefix.index=low&prefix.index=high&prefix[high].Id=11&prefix[low].Id=10")] [InlineData("?prefix.index=index&prefix.index=indexer&prefix[indexer].Id=11&prefix[index].Id=10")] public async Task CollectionModelBinder_BindsListOfComplexType_ExplicitPrefix_Success(string queryString) { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", BindingInfo = new BindingInfo() { BinderModelName = "prefix", }, ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Equal(11, model[1].Id); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Fact] public async Task CollectionModelBinder_BindsListOfComplexType_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.Empty(Assert.IsType>(modelBindingResult.Model)); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } private class Person2 { public int Id { get; set; } [BindRequired] public string Name { get; set; } } [Fact] public async Task CollectionModelBinder_BindsListOfComplexType_WithRequiredProperty_WithPrefix_PartialData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter[0].Id=10¶meter[1].Id=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Equal(11, model[1].Id); Assert.Null(model[0].Name); Assert.Null(model[1].Name); Assert.Equal(4, modelState.Count); Assert.Equal(2, modelState.ErrorCount); Assert.False(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Id").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1].Id").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); var error = Assert.Single(entry.Errors); Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); error = Assert.Single(entry.Errors); Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); } [Fact] public async Task CollectionModelBinder_BindsListOfComplexType_WithRequiredProperty_WithExplicitPrefix_PartialData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", BindingInfo = new BindingInfo() { BinderModelName = "prefix", }, ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?prefix[0].Id=10&prefix[1].Id=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Null(model[0].Name); Assert.Equal(11, model[1].Id); Assert.Null(model[1].Name); Assert.Equal(4, modelState.Count); Assert.Equal(2, modelState.ErrorCount); Assert.False(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Id").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[1].Id").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[1].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); } [Fact] public async Task CollectionModelBinder_BindsCollectionOfComplexType_WithRequiredProperty_EmptyPrefix_PartialData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?[0].Id=10&[1].Id=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Null(model[0].Name); Assert.Equal(11, model[1].Id); Assert.Null(model[1].Name); Assert.Equal(4, modelState.Count); Assert.Equal(2, modelState.ErrorCount); Assert.False(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Id").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "[1].Id").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "[1].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); } [Fact] public async Task CollectionModelBinder_BindsListOfSimpleType_WithIndex_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter.index=low¶meter.index=high¶meter[low]=10¶meter[high]=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new List() { 10, 11 }, model); // "index" is not stored in ModelState. Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[low]").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); Assert.Equal(ModelValidationState.Valid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[high]").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } [Fact] public async Task CollectionModelBinder_BindsCollectionOfComplexType_WithRequiredProperty_WithIndex_PartialData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?index=low&index=high&[high].Id=11&[low].Id=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(10, model[0].Id); Assert.Null(model[0].Name); Assert.Equal(11, model[1].Id); Assert.Null(model[1].Name); Assert.Equal(4, modelState.Count); Assert.Equal(2, modelState.ErrorCount); Assert.False(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "[low].Id").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "[high].Id").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "[low].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "[high].Name").Value; Assert.Null(entry.RawValue); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); } [Fact] public async Task CollectionModelBinder_BindsListOfComplexType_WithRequiredProperty_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.Empty(Assert.IsType>(modelBindingResult.Model)); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } private class Person4 { public IList Addresses { get; set; } } private class Address4 { public int Zip { get; set; } public string Street { get; set; } } [Fact] public async Task CollectionModelBinder_UsesCustomIndexes() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Person4) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { var formCollection = new FormCollection(new Dictionary() { { "Addresses.index", new [] { "Key1", "Key2" } }, { "Addresses[Key1].Street", new [] { "Street1" } }, { "Addresses[Key2].Street", new [] { "Street2" } }, }); request.Form = formCollection; request.ContentType = "application/x-www-form-urlencoded"; }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.IsType(modelBindingResult.Model); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key1].Street").Value; Assert.Equal("Street1", entry.AttemptedValue); Assert.Equal("Street1", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key2].Street").Value; Assert.Equal("Street2", entry.AttemptedValue); Assert.Equal("Street2", entry.RawValue); } private class Person5 { public IList Addresses { get; set; } } private class Address5 { public int Zip { get; set; } [StringLength(3)] public string Street { get; set; } } [Fact] public async Task CollectionModelBinder_UsesCustomIndexes_AddsErrorsWithCorrectKeys() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Person5) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { var formCollection = new FormCollection(new Dictionary() { { "Addresses.index", new [] { "Key1" } }, { "Addresses[Key1].Street", new [] { "Street1" } }, }); request.Form = formCollection; request.ContentType = "application/x-www-form-urlencoded"; }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.IsType(modelBindingResult.Model); Assert.False(modelState.IsValid); var kvp = Assert.Single(modelState); Assert.Equal("Addresses[Key1].Street", kvp.Key); var entry = kvp.Value; var error = Assert.Single(entry.Errors); Assert.Equal(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 3, "Street"), error.ErrorMessage); } [Theory] [InlineData("?[0].Street=LongStreet")] [InlineData("?index=low&[low].Street=LongStreet")] [InlineData("?parameter[0].Street=LongStreet")] [InlineData("?parameter.index=low¶meter[low].Street=LongStreet")] [InlineData("?parameter.index=index¶meter[index].Street=LongStreet")] public async Task CollectionModelBinder_BindsCollectionOfComplexType_ImpliedPrefix_FindsValidationErrors( string queryString) { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection), }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); var address = Assert.Single(model); Assert.Equal("LongStreet", address.Street); Assert.False(modelState.IsValid); var entry = Assert.Single(modelState).Value; var error = Assert.Single(entry.Errors); Assert.Equal(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 3, "Street"), error.ErrorMessage); } // parameter type, form content, expected type public static TheoryData, Type> CollectionTypeData { get { return new TheoryData, Type> { { typeof(IEnumerable), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(List) }, { typeof(ICollection), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(List) }, { typeof(IReadOnlyCollection), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(List) }, { typeof(IList), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(List) }, { typeof(IReadOnlyList), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(List) }, { typeof(List), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(List) }, { typeof(ClosedGenericCollection), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(ClosedGenericCollection) }, { typeof(ClosedGenericList), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(ClosedGenericList) }, { typeof(ExplicitClosedGenericCollection), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(ExplicitClosedGenericCollection) }, { typeof(ExplicitClosedGenericList), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(ExplicitClosedGenericList) }, { typeof(ExplicitCollection), new Dictionary { { "[0]", new[] { "hello" } }, { "[1]", new[] { "world" } }, }, typeof(ExplicitCollection) }, { typeof(ExplicitList), new Dictionary { { "index", new[] { "low", "high" } }, { "[low]", new[] { "hello" } }, { "[high]", new[] { "world" } }, }, typeof(ExplicitList) }, { typeof(IEnumerable), new Dictionary { { string.Empty, new[] { "hello", "world" } }, }, typeof(List) }, { typeof(ICollection), new Dictionary { { "[]", new[] { "hello", "world" } }, }, typeof(List) }, { typeof(IList), new Dictionary { { string.Empty, new[] { "hello", "world" } }, }, typeof(List) }, { typeof(List), new Dictionary { { "[]", new[] { "hello", "world" } }, }, typeof(List) }, { typeof(ClosedGenericCollection), new Dictionary { { string.Empty, new[] { "hello", "world" } }, }, typeof(ClosedGenericCollection) }, { typeof(ClosedGenericList), new Dictionary { { "[]", new[] { "hello", "world" } }, }, typeof(ClosedGenericList) }, { typeof(ExplicitClosedGenericCollection), new Dictionary { { string.Empty, new[] { "hello", "world" } }, }, typeof(ExplicitClosedGenericCollection) }, { typeof(ExplicitClosedGenericList), new Dictionary { { "[]", new[] { "hello", "world" } }, }, typeof(ExplicitClosedGenericList) }, { typeof(ExplicitCollection), new Dictionary { { string.Empty, new[] { "hello", "world" } }, }, typeof(ExplicitCollection) }, { typeof(ExplicitList), new Dictionary { { "[]", new[] { "hello", "world" } }, }, typeof(ExplicitList) }, }; } } [Theory] [MemberData(nameof(CollectionTypeData))] public async Task CollectionModelBinder_BindsParameterToExpectedType( Type parameterType, Dictionary formContent, Type expectedType) { // Arrange var expectedCollection = new List { "hello", "world" }; var parameter = new ParameterDescriptor { Name = "parameter", ParameterType = parameterType, }; var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var testContext = ModelBindingTestHelper.GetTestContext(request => { request.Form = new FormCollection(formContent); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.IsType(expectedType, modelBindingResult.Model); var model = modelBindingResult.Model as IEnumerable; Assert.NotNull(model); // Guard Assert.Equal(expectedCollection, model); Assert.True(modelState.IsValid); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); } private class ClosedGenericCollection : Collection { } private class ClosedGenericList : List { } private class ExplicitClosedGenericCollection : ICollection { private List _data = new List(); int ICollection.Count { get { throw new NotImplementedException(); } } bool ICollection.IsReadOnly { get { return false; } } void ICollection.Add(string item) { _data.Add(item); } void ICollection.Clear() { _data.Clear(); } bool ICollection.Contains(string item) { throw new NotImplementedException(); } void ICollection.CopyTo(string[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_data).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } bool ICollection.Remove(string item) { throw new NotImplementedException(); } } private class ExplicitClosedGenericList : IList { private List _data = new List(); string IList.this[int index] { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } int ICollection.Count { get { throw new NotImplementedException(); } } bool ICollection.IsReadOnly { get { return false; } } void ICollection.Add(string item) { _data.Add(item); } void ICollection.Clear() { _data.Clear(); } bool ICollection.Contains(string item) { throw new NotImplementedException(); } void ICollection.CopyTo(string[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_data).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } int IList.IndexOf(string item) { throw new NotImplementedException(); } void IList.Insert(int index, string item) { throw new NotImplementedException(); } bool ICollection.Remove(string item) { throw new NotImplementedException(); } void IList.RemoveAt(int index) { throw new NotImplementedException(); } } private class ExplicitCollection : ICollection { private List _data = new List(); int ICollection.Count { get { throw new NotImplementedException(); } } bool ICollection.IsReadOnly { get { return false; } } void ICollection.Add(T item) { _data.Add(item); } void ICollection.Clear() { _data.Clear(); } bool ICollection.Contains(T item) { throw new NotImplementedException(); } void ICollection.CopyTo(T[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_data).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } bool ICollection.Remove(T item) { throw new NotImplementedException(); } } private class ExplicitList : IList { private List _data = new List(); T IList.this[int index] { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } int ICollection.Count { get { throw new NotImplementedException(); } } bool ICollection.IsReadOnly { get { return false; } } void ICollection.Add(T item) { _data.Add(item); } void ICollection.Clear() { _data.Clear(); } bool ICollection.Contains(T item) { throw new NotImplementedException(); } void ICollection.CopyTo(T[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_data).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } int IList.IndexOf(T item) { throw new NotImplementedException(); } void IList.Insert(int index, T item) { throw new NotImplementedException(); } bool ICollection.Remove(T item) { throw new NotImplementedException(); } void IList.RemoveAt(int index) { throw new NotImplementedException(); } } } }