// 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.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Xunit; namespace Microsoft.AspNetCore.Mvc.IntegrationTests { // Integration tests targeting the behavior of the DictionaryModelBinder with other model binders. public class DictionaryModelBinderIntegrationTest { [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndKVP_Success() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter[0].Key=key0¶meter[0].Value=10"); }); 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(new Dictionary() { { "key0", 10 } }, 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].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndItem_Success() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter[key0]=10"); }); 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(new Dictionary() { { "key0", 10 } }, model); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var kvp = Assert.Single(modelState); Assert.Equal("parameter[key0]", kvp.Key); var entry = kvp.Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithIndex_Success() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value=10"); }); 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(new Dictionary() { { "key0", 10 } }, 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].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); Assert.Equal(ModelValidationState.Valid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[low].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } [Theory] [InlineData("?prefix[key0]=10")] [InlineData("?prefix[0].Key=key0&prefix[0].Value=10")] [InlineData("?prefix.index=low&prefix[low].Key=key0&prefix[low].Value=10")] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success( string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", BindingInfo = new BindingInfo() { BinderModelName = "prefix", }, ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { 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(new Dictionary() { { "key0", 10 }, }, model); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Theory] [InlineData("?[key0]=10")] [InlineData("?[0].Key=key0&[0].Value=10")] [InlineData("?index=low&[low].Key=key0&[low].Value=10")] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success(string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { 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(new Dictionary() { { "key0", 10 }, }, model); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_NoData() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new 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.Empty(model); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } private class Person { [Range(minimum: 0, maximum: 15, ErrorMessage = "You're out of range.")] public int Id { get; set; } public override bool Equals(object obj) { var other = obj as Person; return other != null && Id == other.Id; } public override int GetHashCode() { return Id.GetHashCode(); } public override string ToString() { return $"{{ { Id } }}"; } } [Theory] [InlineData("?[key0].Id=10")] [InlineData("?[0].Key=key0&[0].Value.Id=10")] [InlineData("?index=low&[low].Key=key0&[low].Value.Id=10")] [InlineData("?parameter[key0].Id=10")] [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=10")] [InlineData("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=10")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_Success(string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { 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(new Dictionary { { "key0", new Person { Id = 10 } }, }, model); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Theory] [InlineData("?prefix[key0].Id=10")] [InlineData("?prefix[0].Key=key0&prefix[0].Value.Id=10")] [InlineData("?prefix.index=low&prefix[low].Key=key0&prefix[low].Value.Id=10")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ExplicitPrefix_Success( string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", BindingInfo = new BindingInfo() { BinderModelName = "prefix", }, ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { 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(new Dictionary { { "key0", new Person { Id = 10 } }, }, model); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } [Theory] [InlineData("?[key0].Id=100")] [InlineData("?[0].Key=key0&[0].Value.Id=100")] [InlineData("?index=low&[low].Key=key0&[low].Value.Id=100")] [InlineData("?parameter[key0].Id=100")] [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=100")] [InlineData("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=100")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_FindsValidationErrors( string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { 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(new Dictionary { { "key0", new Person { Id = 100 } }, }, model); Assert.NotEmpty(modelState); Assert.False(modelState.IsValid); Assert.All(modelState, kvp => { Assert.NotEqual(ModelValidationState.Unvalidated, kvp.Value.ValidationState); Assert.NotEqual(ModelValidationState.Skipped, kvp.Value.ValidationState); }); var entry = Assert.Single(modelState, kvp => kvp.Value.ValidationState == ModelValidationState.Invalid); var error = Assert.Single(entry.Value.Errors); Assert.Equal("You're out of range.", error.ErrorMessage); } [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_NoData() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new 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.Empty(model); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } // parameter type, query string, expected type public static TheoryData DictionaryTypeData { get { return new TheoryData { { typeof(IDictionary), "?[key0]=hello&[key1]=world", typeof(Dictionary) }, { typeof(Dictionary), "?[key0]=hello&[key1]=world", typeof(Dictionary) }, { typeof(ClosedGenericDictionary), "?[key0]=hello&[key1]=world", typeof(ClosedGenericDictionary) }, { typeof(ClosedGenericKeyDictionary), "?[key0]=hello&[key1]=world", typeof(ClosedGenericKeyDictionary) }, { typeof(ExplicitClosedGenericDictionary), "?[key0]=hello&[key1]=world", typeof(ExplicitClosedGenericDictionary) }, { typeof(ExplicitDictionary), "?[key0]=hello&[key1]=world", typeof(ExplicitDictionary) }, { typeof(IDictionary), "?index=low&index=high&[low].Key=key0&[low].Value=hello&[high].Key=key1&[high].Value=world", typeof(Dictionary) }, { typeof(Dictionary), "?[0].Key=key0&[0].Value=hello&[1].Key=key1&[1].Value=world", typeof(Dictionary) }, { typeof(ClosedGenericDictionary), "?index=low&index=high&[low].Key=key0&[low].Value=hello&[high].Key=key1&[high].Value=world", typeof(ClosedGenericDictionary) }, { typeof(ClosedGenericKeyDictionary), "?[0].Key=key0&[0].Value=hello&[1].Key=key1&[1].Value=world", typeof(ClosedGenericKeyDictionary) }, { typeof(ExplicitClosedGenericDictionary), "?index=low&index=high&[low].Key=key0&[low].Value=hello&[high].Key=key1&[high].Value=world", typeof(ExplicitClosedGenericDictionary) }, { typeof(ExplicitDictionary), "?[0].Key=key0&[0].Value=hello&[1].Key=key1&[1].Value=world", typeof(ExplicitDictionary) }, }; } } [Theory] [MemberData(nameof(DictionaryTypeData))] public async Task DictionaryModelBinder_BindsParameterToExpectedType( Type parameterType, string queryString, Type expectedType) { // Arrange var expectedDictionary = new Dictionary { { "key0", "hello" }, { "key1", "world" }, }; var parameter = new ParameterDescriptor { Name = "parameter", ParameterType = parameterType, }; var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString(queryString); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); Assert.IsType(expectedType, modelBindingResult.Model); var model = modelBindingResult.Model as IDictionary; Assert.NotNull(model); // Guard Assert.Equal(expectedDictionary.Keys, model.Keys); Assert.Equal(expectedDictionary.Values, model.Values); Assert.True(modelState.IsValid); Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); } private class ClosedGenericDictionary : Dictionary { } private class ClosedGenericKeyDictionary : Dictionary { } private class ExplicitClosedGenericDictionary : IDictionary { private IDictionary _data = new Dictionary(); string IDictionary.this[string key] { get { throw new NotImplementedException(); } set { _data[key] = value; } } int ICollection>.Count { get { return _data.Count; } } bool ICollection>.IsReadOnly { get { return false; } } ICollection IDictionary.Keys { get { return _data.Keys; } } ICollection IDictionary.Values { get { return _data.Values; } } void ICollection>.Add(KeyValuePair item) { _data.Add(item); } void IDictionary.Add(string key, string value) { throw new NotImplementedException(); } void ICollection>.Clear() { _data.Clear(); } bool ICollection>.Contains(KeyValuePair item) { throw new NotImplementedException(); } bool IDictionary.ContainsKey(string key) { throw new NotImplementedException(); } void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } IEnumerator> IEnumerable>.GetEnumerator() { return _data.GetEnumerator(); } bool ICollection>.Remove(KeyValuePair item) { throw new NotImplementedException(); } bool IDictionary.Remove(string key) { throw new NotImplementedException(); } bool IDictionary.TryGetValue(string key, out string value) { return _data.TryGetValue(key, out value); } } private class ExplicitDictionary : IDictionary { private IDictionary _data = new Dictionary(); TValue IDictionary.this[TKey key] { get { throw new NotImplementedException(); } set { _data[key] = value; } } int ICollection>.Count { get { return _data.Count; } } bool ICollection>.IsReadOnly { get { return false; } } ICollection IDictionary.Keys { get { return _data.Keys; } } ICollection IDictionary.Values { get { return _data.Values; } } void ICollection>.Add(KeyValuePair item) { _data.Add(item); } void IDictionary.Add(TKey key, TValue value) { throw new NotImplementedException(); } void ICollection>.Clear() { _data.Clear(); } bool ICollection>.Contains(KeyValuePair item) { throw new NotImplementedException(); } bool IDictionary.ContainsKey(TKey key) { throw new NotImplementedException(); } void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return _data.GetEnumerator(); } IEnumerator> IEnumerable>.GetEnumerator() { return _data.GetEnumerator(); } bool ICollection>.Remove(KeyValuePair item) { throw new NotImplementedException(); } bool IDictionary.Remove(TKey key) { throw new NotImplementedException(); } bool IDictionary.TryGetValue(TKey key, out TValue value) { return _data.TryGetValue(key, out value); } } } }