ModelBinding: Remove IsReadOnly checks and add/update tests

This commit is contained in:
Kiran Challa 2016-07-11 13:15:44 -07:00
parent be5deef584
commit ac98417398
17 changed files with 906 additions and 103 deletions

View File

@ -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);

View File

@ -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()))

View File

@ -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<IFormFile>(bindingContext))
{
// Silently fail if unable to create an instance or use the current instance.

View File

@ -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<T>
// 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<T> intermediate and set
// property to an array created from that.
return writeable;
return true;
}
if (!typeof(IEnumerable<T>).IsAssignableFrom(modelType))
@ -542,12 +542,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
// public IEnumerable<T> Property { get; set; } = new T[0];
if (modelType.IsAssignableFrom(typeof(List<T>)))
{
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<T>).IsAssignableFrom(modelType);
}

View File

@ -43,6 +43,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
[InlineData(typeof(IList<int>))]
[InlineData(typeof(List<int>))]
[InlineData(typeof(Collection<int>))]
[InlineData(typeof(IEnumerable<int>))]
[InlineData(typeof(IReadOnlyCollection<int>))]
[InlineData(typeof(IReadOnlyList<int>))]
public void Create_ForSupportedTypes_ReturnsBinder(Type modelType)
{
// Arrange
@ -66,46 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<CollectionModelBinder<int>>(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<int> Enumerable { get; }
public IReadOnlyCollection<int> ReadOnlyCollection { get; }
public IReadOnlyList<int> ReadOnlyList { get; }
}
}
}

View File

@ -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]

View File

@ -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]

View File

@ -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<int>(bindingContext);
// Assert
Assert.False(result);
Assert.True(result);
}
[Theory]

View File

@ -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<Product>();
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<string, List<string>> Specs { get; set; }
}
}
}

View File

@ -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&parameter.Aliases[0]=bill&parameter.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<PersonWithReadOnlyAndInitializedProperty>(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));
}
}
}

View File

@ -743,6 +743,16 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
},
typeof(List<string>)
},
{
typeof(IReadOnlyCollection<string>),
new Dictionary<string, StringValues>
{
{ "index", new[] { "low", "high" } },
{ "[low]", new[] { "hello" } },
{ "[high]", new[] { "world" } },
},
typeof(List<string>)
},
{
typeof(IList<string>),
new Dictionary<string, StringValues>
@ -752,6 +762,15 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
},
typeof(List<string>)
},
{
typeof(IReadOnlyList<string>),
new Dictionary<string, StringValues>
{
{ "[0]", new[] { "hello" } },
{ "[1]", new[] { "world" } },
},
typeof(List<string>)
},
{
typeof(List<string>),
new Dictionary<string, StringValues>

View File

@ -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<Car1>(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";

View File

@ -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<string, string[]>)
};
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<KeyValuePair<string, string[]>>(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);
}
}
}

View File

@ -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<string, IEnumerable<SpecDoc>> Specs { get; set; }
}
// Dictionary property with an Array value type
private class Car2
{
public string Name { get; set; }
public Dictionary<string, SpecDoc[]> Specs { get; set; }
}
private class Car3
{
public string Name { get; set; }
public IEnumerable<KeyValuePair<string, IEnumerable<SpecDoc>>> 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<Car1>(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<Car2>(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<Car3>(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<string, Dictionary<string, string>> 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<Car4>(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<string> Aliases { get; }
}
[Theory]
[InlineData("?parameter.ProductId=10")]
[InlineData("?parameter.ProductId=10&parameter.Name=Camera")]
[InlineData("?parameter.ProductId=10&parameter.Name=Camera&parameter.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<Product>(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<string, LocationInfo> 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<Photo>(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));

View File

@ -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> 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> 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> 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()
{

View File

@ -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<string, List<string>>();
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 });
}
}
}

View File

@ -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<string, IEnumerable<IFormFile>> Specs { get; set; }
}
}