// 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.Generic; using System.Diagnostics; 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 GenericModelBinder and related classes // with other model binders. public class GenericModelBinderIntegrationTest { // This isn't an especially useful scenario - but it exercises what happens when you // try to use a Collection of something that is bound greedily by model-type. // // In this example we choose IFormCollection because IFormCollection has a dedicated // model binder. [Fact] public async Task GenericModelBinder_BindsCollection_ElementTypeFromGreedyModelBinder_WithPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; // Need to have a key here so that the GenericModelBinder will recurse to bind elements. var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter.index=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); var formCollection = Assert.Single(model); Assert.NotNull(formCollection); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); Assert.Empty(modelState); } // This isn't an especially useful scenario - but it exercises what happens when you // try to use a Collection of something that is bound greedily by model-type. // // In this example we choose IFormCollection - because IFormCollection has a dedicated // model binder. [Fact] public async Task GenericModelBinder_BindsCollection_ElementTypeFromGreedyModelBinder_EmptyPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; // Need to have a key here so that the GenericModelBinder will recurse to bind elements. var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?index=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); var formCollection = Assert.Single(model); Assert.NotNull(formCollection); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); Assert.Empty(modelState); } // This isn't an especially useful scenario - but it exercises what happens when you // try to use a Collection of something that is bound greedily by model-type. // // In this example we choose IFormCollection - because IFormCollection has a dedicated // model binder. [Fact] public async Task GenericModelBinder_BindsCollection_ElementTypeFromGreedyModelBinder_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(List) }; // Without a key here so the GenericModelBinder will not recurse to bind elements. var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); Assert.Empty(model); } [BindAddress] private class Address { } private class BindAddressAttribute : Attribute, IBindingSourceMetadata { public static readonly BindingSource Source = new BindingSource( "Address", displayName: "Address", isGreedy: true, isFromRequest: true); public BindingSource BindingSource { get { return Source; } } } private class AddressBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { var allowedBindingSource = context.BindingInfo.BindingSource; if (allowedBindingSource?.CanAcceptDataFrom(BindAddressAttribute.Source) == true) { // Binding Sources are opt-in. This model either didn't specify one or specified something // incompatible so let other binders run. return new AddressBinder(); } return null; } } private class AddressBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } Debug.Assert(bindingContext.Result == ModelBindingResult.Failed()); var allowedBindingSource = bindingContext.BindingSource; if (allowedBindingSource == null || !allowedBindingSource.CanAcceptDataFrom(BindAddressAttribute.Source)) { // Binding Sources are opt-in. This model either didn't specify one or specified something // incompatible so let other binders run. return Task.CompletedTask; } bindingContext.Result = ModelBindingResult.Success(new Address()); return Task.CompletedTask; } } // This isn't an especially useful scenario - but it exercises what happens when you // try to use a Collection of something that is bound greedily by binding source. [Fact] public async Task GenericModelBinder_BindsCollection_ElementTypeUsesGreedyModelBinder_WithPrefix_Success() { // Arrange // Need to have a key here so that the GenericModelBinder will recurse to bind elements. var testContext = ModelBindingTestHelper.GetTestContext( request => request.QueryString = new QueryString("?parameter.index=0")); var modelState = testContext.ModelState; var parameterBinder = ModelBindingTestHelper.GetParameterBinder( testContext.MvcOptions, new AddressBinderProvider()); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Address[]) }; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType(modelBindingResult.Model); Assert.Single(model); Assert.NotNull(model[0]); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } // Similar to the GenericModelBinder_BindsCollection_ElementTypeUsesGreedyModelBinder_WithPrefix_Success // scenario but mis-configured. Model using a BindingSource for which no ModelBinder is enabled. [Fact] public async Task GenericModelBinder_BindsCollection_ElementTypeUsesGreedyBindingSource_WithPrefix_NullElement() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Address[]) }; // Need to have a key here so that the GenericModelBinder will recurse to bind elements. var testContext = ModelBindingTestHelper.GetTestContext( request => request.QueryString = new QueryString("?parameter.index=0")); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType(modelBindingResult.Model); Assert.Single(model); Assert.Null(model[0]); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsArrayOfDictionary_WithPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary[]) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?parameter[0][0].Key=key0¶meter[0][0].Value=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType[]>(modelBindingResult.Model); var dictionary = Assert.Single(model); var kvp = Assert.Single(dictionary); Assert.Equal("key0", kvp.Key); Assert.Equal(10, kvp.Value); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "parameter[0][0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "parameter[0][0].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsArrayOfDictionary_EmptyPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary[]) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?[0][0].Key=key0&[0][0].Value=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType[]>(modelBindingResult.Model); var dictionary = Assert.Single(model); var kvp = Assert.Single(dictionary); Assert.Equal("key0", kvp.Key); Assert.Equal(10, kvp.Value); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "[0][0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "[0][0].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsArrayOfDictionary_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); 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 parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType[]>(modelBindingResult.Model); Assert.NotNull(model); Assert.Empty(model); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsCollectionOfKeyValuePair_WithPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection>) }; 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 parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); var kvp = Assert.Single(model); Assert.Equal("key0", kvp.Key); Assert.Equal(10, kvp.Value); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsCollectionOfKeyValuePair_EmptyPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection>) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?[0].Key=key0&[0].Value=10"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); var kvp = Assert.Single(model); Assert.Equal("key0", kvp.Key); Assert.Equal(10, kvp.Value); Assert.Equal(2, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "[0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "[0].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsCollectionOfKeyValuePair_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(ICollection>) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); Assert.NotNull(model); Assert.Empty(model); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsDictionaryOfList_WithPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); 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[0]=10¶meter[0].Value[1]=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); var kvp = Assert.Single(model); Assert.Equal("key0", kvp.Key); Assert.Equal(new List() { 10, 11 }, kvp.Value); Assert.Equal(3, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value[0]").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value[1]").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsDictionaryOfList_EmptyPrefix_Success() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameter = new ParameterDescriptor() { Name = "parameter", ParameterType = typeof(Dictionary>) }; var testContext = ModelBindingTestHelper.GetTestContext(request => { request.QueryString = new QueryString("?[0].Key=key0&[0].Value[0]=10&[0].Value[1]=11"); }); var modelState = testContext.ModelState; // Act var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); var kvp = Assert.Single(model); Assert.Equal("key0", kvp.Key); Assert.Equal(new List() { 10, 11 }, kvp.Value); Assert.Equal(3, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); var entry = Assert.Single(modelState, e => e.Key == "[0].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "[0].Value[0]").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); entry = Assert.Single(modelState, e => e.Key == "[0].Value[1]").Value; Assert.Equal("11", entry.AttemptedValue); Assert.Equal("11", entry.RawValue); } // This is part of a random sampling of scenarios where a GenericModelBinder is used // recursively. [Fact] public async Task GenericModelBinder_BindsDictionaryOfList_NoData() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); 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 parameterBinder.BindModelAsync(parameter, testContext); // Assert Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>>(modelBindingResult.Model); Assert.NotNull(model); Assert.Empty(model); Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } } }