diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs index a6f087883b..33267fd069 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinderProvider.cs @@ -18,9 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders throw new ArgumentNullException(nameof(context)); } - // We don't support binding readonly properties of arrays because we can't resize the - // existing value. - if (context.Metadata.ModelType.IsArray && !context.Metadata.IsReadOnly) + if (context.Metadata.ModelType.IsArray) { var elementType = context.Metadata.ElementMetadata.ModelType; var elementBinder = context.CreateBinder(context.Metadata.ElementMetadata); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs index 17c5e0daf8..cb80107ff3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinderProvider.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } var modelType = context.Metadata.ModelType; - + // Arrays are handled by another binder. if (modelType.IsArray) { @@ -43,10 +43,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders // If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since // that's what we would create. (The cases handled here are IEnumerable<>, IReadOnlyColection<> and // IReadOnlyList<>). - // - // We need to check IsReadOnly because we need to know if we can SET the property. var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(IEnumerable<>)); - if (enumerableType != null && !context.Metadata.IsReadOnly) + if (enumerableType != null) { var listType = typeof(List<>).MakeGenericType(enumerableType.GenericTypeArguments); if (modelType.GetTypeInfo().IsAssignableFrom(listType.GetTypeInfo())) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs index 4d3a4ead4d..17c1cfbebb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormFileModelBinder.cs @@ -29,9 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders throw new ArgumentNullException(nameof(bindingContext)); } - var createFileCollection = - bindingContext.ModelType == typeof(IFormFileCollection) && - !bindingContext.ModelMetadata.IsReadOnly; + var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection); if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection(bindingContext)) { // Silently fail if unable to create an instance or use the current instance. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Internal/ModelBindingHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Internal/ModelBindingHelper.cs index c0520448fb..be01d81d8c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Internal/ModelBindingHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Internal/ModelBindingHelper.cs @@ -509,19 +509,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal { var model = bindingContext.Model; var modelType = bindingContext.ModelType; - var writeable = !bindingContext.ModelMetadata.IsReadOnly; + if (typeof(T).IsAssignableFrom(modelType)) { // Scalar case. Existing model is not relevant and property must always be set. Will use a List // intermediate and set property to first element, if any. - return writeable; + return true; } if (modelType == typeof(T[])) { // Can't change the length of an existing array or replace it. Will use a List intermediate and set // property to an array created from that. - return writeable; + return true; } if (!typeof(IEnumerable).IsAssignableFrom(modelType)) @@ -542,12 +542,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal // public IEnumerable Property { get; set; } = new T[0]; if (modelType.IsAssignableFrom(typeof(List))) { - return writeable; + return true; } // Will we be able to activate an instance and use that? - return writeable && - modelType.GetTypeInfo().IsClass && + return modelType.GetTypeInfo().IsClass && !modelType.GetTypeInfo().IsAbstract && typeof(ICollection).IsAssignableFrom(modelType); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs index 644e4b456c..9d6efc538d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs @@ -43,6 +43,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders [InlineData(typeof(IList))] [InlineData(typeof(List))] [InlineData(typeof(Collection))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(IReadOnlyCollection))] + [InlineData(typeof(IReadOnlyList))] public void Create_ForSupportedTypes_ReturnsBinder(Type modelType) { // Arrange @@ -66,46 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.IsType>(result); } - // These aren't ICollection<> - we can handle them by creating a List<> - but in this case - // we can't set the property so we can't bind. - [Theory] - [InlineData(nameof(ReadOnlyProperties.Enumerable))] - [InlineData(nameof(ReadOnlyProperties.ReadOnlyCollection))] - [InlineData(nameof(ReadOnlyProperties.ReadOnlyList))] - public void Create_ForNonICollectionTypes_ReadOnlyProperty_ReturnsNull(string propertyName) - { - // Arrange - var provider = new CollectionModelBinderProvider(); - - var metadataProvider = TestModelBinderProviderContext.CachedMetadataProvider; - - var metadata = metadataProvider.GetMetadataForProperty(typeof(ReadOnlyProperties), propertyName); - Assert.NotNull(metadata); - Assert.True(metadata.IsReadOnly); - - var context = new TestModelBinderProviderContext(metadata, bindingInfo: null); - - // Act - var result = provider.GetBinder(context); - - // Assert - Assert.Null(result); - } - private class Person { public string Name { get; set; } public int Age { get; set; } } - - private class ReadOnlyProperties - { - public IEnumerable Enumerable { get; } - - public IReadOnlyCollection ReadOnlyCollection { get; } - - public IReadOnlyList ReadOnlyList { get; } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs index 4d9f9f5523..ac15b3fe2a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FormFileModelBinderTest.cs @@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Fact] - public async Task FormFileModelBinder_ReturnsFailedResult_ForReadOnlyDestination() + public async Task FormFileModelBinder_ReturnsResult_ForReadOnlyDestination() { // Arrange var binder = new FormFileModelBinder(); @@ -217,8 +217,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders await binder.BindModelAsync(bindingContext); // Assert - Assert.False(bindingContext.Result.IsModelSet); - Assert.Null(bindingContext.Result.Model); + Assert.True(bindingContext.Result.IsModelSet); + Assert.NotNull(bindingContext.Result.Model); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs index f1099c9723..f04680cc19 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/HeaderModelBinderTests.cs @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Fact] - public async Task HeaderBinder_ReturnsFailedResult_ForReadOnlyDestination() + public async Task HeaderBinder_ReturnsResult_ForReadOnlyDestination() { // Arrange var header = "Accept"; @@ -116,8 +116,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders await binder.BindModelAsync(bindingContext); // Assert - Assert.False(bindingContext.Result.IsModelSet); - Assert.Null(bindingContext.Result.Model); + Assert.True(bindingContext.Result.IsModelSet); + Assert.NotNull(bindingContext.Result.Model); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 919a422a50..d2354d1390 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -1412,7 +1412,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding [InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ListProperty))] [InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ScalarProperty))] [InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ScalarPropertyWithValue))] - public void CanGetCompatibleCollection_ReturnsFalse_IfReadOnly(string propertyName) + public void CanGetCompatibleCollection_ReturnsTrue_IfReadOnly(string propertyName) { // Arrange var bindingContext = GetBindingContextForProperty(propertyName); @@ -1421,7 +1421,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var result = ModelBindingHelper.CanGetCompatibleCollection(bindingContext); // Assert - Assert.False(result); + Assert.True(result); } [Theory] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FormFileUploadTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FormFileUploadTest.cs index 8370a1720d..9e4208693c 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FormFileUploadTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FormFileUploadTest.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -41,11 +42,50 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("John's biography content", user.Biography); } + [Fact] + public async Task UploadMultipleFiles() + { + // Arrange + var content = new MultipartFormDataContent(); + content.Add(new StringContent("Phone"), "Name"); + content.Add(new StringContent("camera"), "Specs[0].Key"); + content.Add(new StringContent("camera spec1 file contents"), "Specs[0].Value", "camera_spec1.txt"); + content.Add(new StringContent("camera spec2 file contents"), "Specs[0].Value", "camera_spec2.txt"); + content.Add(new StringContent("battery"), "Specs[1].Key"); + content.Add(new StringContent("battery spec1 file contents"), "Specs[1].Value", "battery_spec1.txt"); + content.Add(new StringContent("battery spec2 file contents"), "Specs[1].Value", "battery_spec2.txt"); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/UploadProductSpecs"); + request.Content = content; + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var product = await response.Content.ReadAsAsync(); + Assert.NotNull(product); + Assert.Equal("Phone", product.Name); + Assert.NotNull(product.Specs); + Assert.Equal(2, product.Specs.Count); + Assert.True(product.Specs.ContainsKey("camera")); + Assert.Equal(new[] { "camera_spec1.txt", "camera_spec2.txt" }, product.Specs["camera"]); + Assert.True(product.Specs.ContainsKey("battery")); + Assert.Equal(new[] { "battery_spec1.txt", "battery_spec2.txt" }, product.Specs["battery"]); + } + private class User { public string Name { get; set; } public int Age { get; set; } public string Biography { get; set; } } + + private class Product + { + public string Name { get; set; } + + public Dictionary> Specs { get; set; } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs index 20bf798cdc..20709de70a 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs @@ -327,5 +327,47 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } + + private class PersonWithReadOnlyAndInitializedProperty + { + public string Name { get; set; } + + public string[] Aliases { get; } = new[] { "Alias1", "Alias2" }; + } + + [Fact] + public async Task ArrayModelBinder_BindsArrayOfComplexTypeHavingInitializedData_WithPrefix_Success_ReadOnly() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(PersonWithReadOnlyAndInitializedProperty) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString("?parameter.Name=James¶meter.Aliases[0]=bill¶meter.Aliases[1]=william"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + Assert.True(modelState.IsValid); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("James", model.Name); + Assert.NotNull(model.Aliases); + Assert.Collection( + model.Aliases, + (e) => Assert.Equal("Alias1", e), + (e) => Assert.Equal("Alias2", e)); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs index 067d73a8e9..95f9298337 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs @@ -743,6 +743,16 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests }, typeof(List) }, + { + typeof(IReadOnlyCollection), + new Dictionary + { + { "index", new[] { "low", "high" } }, + { "[low]", new[] { "hello" } }, + { "[high]", new[] { "world" } }, + }, + typeof(List) + }, { typeof(IList), new Dictionary @@ -752,6 +762,15 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests }, typeof(List) }, + { + typeof(IReadOnlyList), + new Dictionary + { + { "[0]", new[] { "hello" } }, + { "[1]", new[] { "world" } }, + }, + typeof(List) + }, { typeof(List), new Dictionary diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs index ea212b7480..2b1218fc43 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs @@ -299,6 +299,62 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Empty(modelState.Keys); } + private class Car1 + { + public string Name { get; set; } + + public FormFileCollection Specs { get; set; } + } + + [Fact] + public async Task BindProperty_WithData_WithPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Car1) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("p.Name", "Accord"); + UpdateRequest(request, data, "p.Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var car = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(car.Specs); + var file = Assert.Single(car.Specs); + Assert.Equal("form-data; name=p.Specs; filename=text.txt", file.ContentDisposition); + var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + Assert.Single(modelState, e => e.Key == "p.Specs"); + } + private void UpdateRequest(HttpRequest request, string data, string name) { const string fileName = "text.txt"; diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs index 1a1cce43c7..1e4d340605 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs @@ -498,5 +498,49 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } + + [Fact] + public async Task KeyValuePairModelBinder_BindsKeyValuePairOfArray_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "p", + ParameterType = typeof(KeyValuePair) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString("?p.Key=key1&p.Value[0]=value1&p.Value[1]=value2"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var model = Assert.IsType>(modelBindingResult.Model); + Assert.Equal("key1", model.Key); + Assert.Equal(new[] { "value1", "value2" }, model.Value); + + Assert.Equal(3, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, kvp => kvp.Key == "p.Key").Value; + Assert.Equal("key1", entry.AttemptedValue); + Assert.Equal("key1", entry.RawValue); + + entry = Assert.Single(modelState, kvp => kvp.Key == "p.Value[0]").Value; + Assert.Equal("value1", entry.AttemptedValue); + Assert.Equal("value1", entry.RawValue); + + entry = Assert.Single(modelState, kvp => kvp.Key == "p.Value[1]").Value; + Assert.Equal("value2", entry.AttemptedValue); + Assert.Equal("value2", entry.RawValue); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs index 6828216779..4509f3ec55 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs @@ -1122,6 +1122,328 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.True(modelState.IsValid); } + // Dictionary property with an IEnumerable<> value type + private class Car1 + { + public string Name { get; set; } + + public Dictionary> Specs { get; set; } + } + + // Dictionary property with an Array value type + private class Car2 + { + public string Name { get; set; } + + public Dictionary Specs { get; set; } + } + + private class Car3 + { + public string Name { get; set; } + + public IEnumerable>> Specs { get; set; } + } + + private class SpecDoc + { + public string Name { get; set; } + } + + [Fact] + public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithIEnumerableComplexTypeValue_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "p", + ParameterType = typeof(Car1) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + var queryString = "?p.Name=Accord" + + "&p.Specs[0].Key=camera_specs" + + "&p.Specs[0].Value[0].Name=camera_spec1.txt" + + "&p.Specs[0].Value[1].Name=camera_spec2.txt" + + "&p.Specs[1].Key=tyre_specs" + + "&p.Specs[1].Value[0].Name=tyre_spec1.txt" + + "&p.Specs[1].Value[1].Name=tyre_spec2.txt"; + request.QueryString = new QueryString(queryString); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("Accord", model.Name); + + Assert.Collection( + model.Specs, + (e) => + { + Assert.Equal("camera_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("camera_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("camera_spec2.txt", s.Name); + }); + }, + (e) => + { + Assert.Equal("tyre_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("tyre_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("tyre_spec2.txt", s.Name); + }); + }); + + Assert.Equal(7, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value; + Assert.Equal("camera_specs", entry.AttemptedValue); + Assert.Equal("camera_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value; + Assert.Equal("camera_spec1.txt", entry.AttemptedValue); + Assert.Equal("camera_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value; + Assert.Equal("camera_spec2.txt", entry.AttemptedValue); + Assert.Equal("camera_spec2.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value; + Assert.Equal("tyre_specs", entry.AttemptedValue); + Assert.Equal("tyre_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value; + Assert.Equal("tyre_spec1.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value; + Assert.Equal("tyre_spec2.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec2.txt", entry.RawValue); + } + + [Fact] + public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithArrayOfComplexTypeValue_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "p", + ParameterType = typeof(Car2) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + var queryString = "?p.Name=Accord" + + "&p.Specs[0].Key=camera_specs" + + "&p.Specs[0].Value[0].Name=camera_spec1.txt" + + "&p.Specs[0].Value[1].Name=camera_spec2.txt" + + "&p.Specs[1].Key=tyre_specs" + + "&p.Specs[1].Value[0].Name=tyre_spec1.txt" + + "&p.Specs[1].Value[1].Name=tyre_spec2.txt"; + request.QueryString = new QueryString(queryString); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("Accord", model.Name); + + Assert.Collection( + model.Specs, + (e) => + { + Assert.Equal("camera_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("camera_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("camera_spec2.txt", s.Name); + }); + }, + (e) => + { + Assert.Equal("tyre_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("tyre_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("tyre_spec2.txt", s.Name); + }); + }); + + Assert.Equal(7, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value; + Assert.Equal("camera_specs", entry.AttemptedValue); + Assert.Equal("camera_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value; + Assert.Equal("camera_spec1.txt", entry.AttemptedValue); + Assert.Equal("camera_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value; + Assert.Equal("camera_spec2.txt", entry.AttemptedValue); + Assert.Equal("camera_spec2.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value; + Assert.Equal("tyre_specs", entry.AttemptedValue); + Assert.Equal("tyre_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value; + Assert.Equal("tyre_spec1.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value; + Assert.Equal("tyre_spec2.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec2.txt", entry.RawValue); + } + + [Fact] + public async Task MutableObjectModelBinder_BindsDictionaryProperty_WithIEnumerableOfKeyValuePair_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "p", + ParameterType = typeof(Car3) + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + var queryString = "?p.Name=Accord" + + "&p.Specs[0].Key=camera_specs" + + "&p.Specs[0].Value[0].Name=camera_spec1.txt" + + "&p.Specs[0].Value[1].Name=camera_spec2.txt" + + "&p.Specs[1].Key=tyre_specs" + + "&p.Specs[1].Value[0].Name=tyre_spec1.txt" + + "&p.Specs[1].Value[1].Name=tyre_spec2.txt"; + request.QueryString = new QueryString(queryString); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("Accord", model.Name); + + Assert.Collection( + model.Specs, + (e) => + { + Assert.Equal("camera_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("camera_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("camera_spec2.txt", s.Name); + }); + }, + (e) => + { + Assert.Equal("tyre_specs", e.Key); + Assert.Collection( + e.Value, + (s) => + { + Assert.Equal("tyre_spec1.txt", s.Name); + }, + (s) => + { + Assert.Equal("tyre_spec2.txt", s.Name); + }); + }); + + Assert.Equal(7, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Key").Value; + Assert.Equal("camera_specs", entry.AttemptedValue); + Assert.Equal("camera_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[0].Name").Value; + Assert.Equal("camera_spec1.txt", entry.AttemptedValue); + Assert.Equal("camera_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[0].Value[1].Name").Value; + Assert.Equal("camera_spec2.txt", entry.AttemptedValue); + Assert.Equal("camera_spec2.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Key").Value; + Assert.Equal("tyre_specs", entry.AttemptedValue); + Assert.Equal("tyre_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[0].Name").Value; + Assert.Equal("tyre_spec1.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs[1].Value[1].Name").Value; + Assert.Equal("tyre_spec2.txt", entry.AttemptedValue); + Assert.Equal("tyre_spec2.txt", entry.RawValue); + } + private class Order8 { public string Name { get; set; } @@ -1294,6 +1616,90 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.True(modelState.IsValid); } + private class Car4 + { + public string Name { get; set; } + + public KeyValuePair> Specs { get; set; } + } + + [Fact] + public async Task Foo_MutableObjectModelBinder_BindsKeyValuePairProperty_WithPrefix_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "p", + ParameterType = typeof(Car4) + }; + + // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements. + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + var queryString = "?p.Name=Accord" + + "&p.Specs.Key=camera_specs" + + "&p.Specs.Value[0].Key=spec1" + + "&p.Specs.Value[0].Value=spec1.txt" + + "&p.Specs.Value[1].Key=spec2" + + "&p.Specs.Value[1].Value=spec2.txt"; + + request.QueryString = new QueryString(queryString); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("Accord", model.Name); + + Assert.Collection( + model.Specs.Value, + (e) => + { + Assert.Equal("spec1", e.Key); + Assert.Equal("spec1.txt", e.Value); + }, + (e) => + { + Assert.Equal("spec2", e.Key); + Assert.Equal("spec2.txt", e.Value); + }); + + Assert.Equal(6, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs.Key").Value; + Assert.Equal("camera_specs", entry.AttemptedValue); + Assert.Equal("camera_specs", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Key").Value; + Assert.Equal("spec1", entry.AttemptedValue); + Assert.Equal("spec1", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[0].Value").Value; + Assert.Equal("spec1.txt", entry.AttemptedValue); + Assert.Equal("spec1.txt", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Key").Value; + Assert.Equal("spec2", entry.AttemptedValue); + Assert.Equal("spec2", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "p.Specs.Value[1].Value").Value; + Assert.Equal("spec2.txt", entry.AttemptedValue); + Assert.Equal("spec2.txt", entry.RawValue); + } + private class Order9 { public Person9 Customer { get; set; } @@ -2126,6 +2532,121 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } + private class Product + { + public int ProductId { get; set; } + + public string Name { get; } + + public IList Aliases { get; } + } + + [Theory] + [InlineData("?parameter.ProductId=10")] + [InlineData("?parameter.ProductId=10¶meter.Name=Camera")] + [InlineData("?parameter.ProductId=10¶meter.Name=Camera¶meter.Aliases[0]=Camera1")] + public async Task ComplexTypeModelBinder_BindsSettableProperties(string queryString) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Product) + }; + + // Need to have a key here so that the ComplexTypeModelBinder will recurse to bind elements. + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString(queryString); + SetJsonBodyContent(request, AddressBodyContent); + }); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(model); + Assert.Equal(10, model.ProductId); + Assert.Null(model.Name); + Assert.Null(model.Aliases); + } + + private class Photo + { + public string Id { get; set; } + + public KeyValuePair Info { get; set; } + } + + private class LocationInfo + { + [FromHeader] + public string GpsCoordinates { get; set; } + + public int Zipcode { get; set; } + } + + [Fact] + public async Task MutableObjectModelBinder_BindsKeyValuePairProperty_HavingFromHeaderProperty_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Photo) + }; + + // Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements. + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.Headers.Add("GpsCoordinates", "10,20"); + request.QueryString = new QueryString("?Id=1&Info.Key=location1&Info.Value.Zipcode=98052"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + // Model + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("1", model.Id); + Assert.NotNull(model.Info); + Assert.Equal("location1", model.Info.Key); + Assert.NotNull(model.Info.Value); + Assert.Equal("10,20", model.Info.Value.GpsCoordinates); + Assert.Equal(98052, model.Info.Value.Zipcode); + + // ModelState + Assert.Equal(4, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "Id").Value; + Assert.Equal("1", entry.AttemptedValue); + Assert.Equal("1", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "Info.Key").Value; + Assert.Equal("location1", entry.AttemptedValue); + Assert.Equal("location1", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "Info.Value.Zipcode").Value; + Assert.Equal("98052", entry.AttemptedValue); + Assert.Equal("98052", entry.RawValue); + + entry = Assert.Single(modelState, e => e.Key == "Info.Value.GpsCoordinates").Value; + Assert.Equal("10,20", entry.AttemptedValue); + Assert.Equal(new[] { "10", "20" }, entry.RawValue); + } + private static void SetJsonBodyContent(HttpRequest request, string content) { var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content)); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs index bc2eeb1d2e..982879aa23 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Collections.Generic; using System.IO; using System.Text; @@ -356,6 +357,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public CustomReadOnlyCollection
Address { get; set; } } + [Fact] public async Task TryUpdateModel_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGetBound() { // Arrange @@ -371,22 +373,46 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var result = await TryUpdateModelAsync(model, string.Empty, testContext); // Assert - Assert.True(result); + Assert.False(result); - // Model - Assert.NotNull(model.Address); - - // Read-only collection should not be updated. - Assert.Empty(model.Address); - - // ModelState (data is valid but is not copied into Address). - Assert.True(modelState.IsValid); + // ModelState + Assert.False(modelState.IsValid); 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(ModelValidationState.Unvalidated, state.ValidationState); Assert.Equal("SomeStreet", state.RawValue); + Assert.Equal("SomeStreet", state.AttemptedValue); + } + + [Fact] + public async Task TryUpdateModel_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound() + { + // Arrange + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = QueryString.Create("prefix.Address[0].Street", "SomeStreet"); + }); + + var modelState = testContext.ModelState; + var model = new Person6(); + + // Act + var result = await TryUpdateModelAsync(model, "prefix", testContext); + + // Assert + Assert.False(result); + + // ModelState + Assert.False(modelState.IsValid); + var entry = Assert.Single(modelState); + Assert.Equal("prefix.Address[0].Street", entry.Key); + var state = entry.Value; + Assert.NotNull(state); + Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState); + Assert.Equal("SomeStreet", state.RawValue); + Assert.Equal("SomeStreet", state.AttemptedValue); } private class Person4 @@ -484,7 +510,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } [Fact] - public async Task TryUpdateModel_NonSettableArrayModel_EmptyPrefix_GetsBound() + public async Task TryUpdateModel_NonSettableArrayModel_EmptyPrefix_IsNotBound() { // Arrange var testContext = ModelBindingTestHelper.GetTestContext(request => @@ -512,6 +538,99 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Empty(modelState); } + private class Person7 + { + public IEnumerable
Address { get; } = new Address[] + { + new Address() + { + City = "Redmond", + Street = "One Microsoft Way" + } + }; + } + + [Fact] + public async Task TryUpdateModel_NonSettableIEnumerableModel_EmptyPrefix_IsNotBound() + { + // Arrange + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = QueryString.Create("Address[0].Street", "SomeStreet"); + }); + + var modelState = testContext.ModelState; + var model = new Person7(); + + // Act + var result = await TryUpdateModelAsync(model, string.Empty, testContext); + + // Assert + Assert.True(result); + + // Model + Assert.NotNull(model.Address); + + // Arrays should not be updated. + Assert.Equal(1, model.Address.Count()); + Assert.Collection( + model.Address, + (a) => + { + Assert.Equal("Redmond", a.City); + Assert.Equal("One Microsoft Way", a.Street); + }); + + // ModelState + Assert.True(modelState.IsValid); + } + + private class Person8 + { + public ICollection
Address { get; } = new Address[] + { + new Address() + { + City = "Redmond", + Street = "One Microsoft Way" + } + }; + } + + [Fact] + public async Task TryUpdateModel_NonSettableICollectionModel_EmptyPrefix_IsNotBound() + { + // Arrange + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = QueryString.Create("Address[0].Street", "SomeStreet"); + }); + + var modelState = testContext.ModelState; + var model = new Person8(); + + // Act + var result = await TryUpdateModelAsync(model, string.Empty, testContext); + + // Assert + Assert.True(result); + + // Model + Assert.NotNull(model.Address); + + // Arrays should not be updated. + Assert.Equal(1, model.Address.Count()); + Assert.Collection( + model.Address, + (a) => + { + Assert.Equal("Redmond", a.City); + Assert.Equal("One Microsoft Way", a.Street); + }); + + // ModelState + Assert.True(modelState.IsValid); + } [Fact] public async Task TryUpdateModel_ExistingModel_WithPrefix_ValuesGetOverwritten() @@ -818,39 +937,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Equal(ModelValidationState.Valid, state.ValidationState); } - public async Task TryUpdateModel_ReadOnlyCollectionModel_WithPrefix_DoesNotGetBound() - { - // Arrange - var testContext = ModelBindingTestHelper.GetTestContext(request => - { - request.QueryString = QueryString.Create("prefix.Address[0].Street", "SomeStreet"); - }); - - var modelState = testContext.ModelState; - var model = new Person6(); - - // Act - var result = await TryUpdateModelAsync(model, "prefix", testContext); - - // Assert - Assert.True(result); - - // Model - Assert.NotNull(model.Address); - - // Read-only collection should not be updated. - Assert.Empty(model.Address); - - // ModelState (data is valid but is not copied into Address). - Assert.True(modelState.IsValid); - 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.RawValue); - } - [Fact] public async Task TryUpdateModel_SettableArrayModel_WithPrefix_CreatesArray() { diff --git a/test/WebSites/FilesWebSite/Controllers/UploadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/UploadFilesController.cs index f0f59514fe..0839978cd0 100644 --- a/test/WebSites/FilesWebSite/Controllers/UploadFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/UploadFilesController.cs @@ -1,6 +1,8 @@ // 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.Linq; +using System.Collections.Generic; using System.Threading.Tasks; using FilesWebSite.Models; using Microsoft.AspNetCore.Mvc; @@ -21,5 +23,22 @@ namespace FilesWebSite.Controllers return Json(resultUser); } + + [HttpPost("UploadProductSpecs")] + public IActionResult ProductSpecs(Product product) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var files = new Dictionary>(); + foreach (var keyValuePair in product.Specs) + { + files.Add(keyValuePair.Key, keyValuePair.Value?.Select(formFile => formFile?.FileName).ToList()); + } + + return Json(new { Name = product.Name, Specs = files }); + } } } diff --git a/test/WebSites/FilesWebSite/Models/Product.cs b/test/WebSites/FilesWebSite/Models/Product.cs new file mode 100644 index 0000000000..8d8921d1b0 --- /dev/null +++ b/test/WebSites/FilesWebSite/Models/Product.cs @@ -0,0 +1,15 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace FilesWebSite.Models +{ + public class Product + { + public string Name { get; set; } + + public IDictionary> Specs { get; set; } + } +}