// 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.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Routing; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal { public class ControllerArgumentBinderTests { [Fact] public async Task BindActionArgumentsAsync_DoesNotAddActionArguments_IfBinderReturnsNull() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.Parameters.Add( new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), }); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Empty(arguments); } [Fact] public async Task BindActionArgumentsAsync_DoesNotAddActionArguments_IfBinderDoesNotSetModel() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.Parameters.Add( new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), }); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Empty(arguments); } [Fact] public async Task BindActionArgumentsAsync_AddsActionArguments_IfBinderReturnsNotNull() { // Arrange Func method = foo => 1; var actionDescriptor = GetActionDescriptor(); actionDescriptor.Parameters.Add( new ParameterDescriptor { Name = "foo", ParameterType = typeof(string), BindingInfo = new BindingInfo(), }); var value = "Hello world"; var metadataProvider = new EmptyModelMetadataProvider(); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string)); context.Result = ModelBindingResult.Success(value); }) .Returns(TaskCache.CompletedTask); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal(1, arguments.Count); Assert.Equal(value, arguments["foo"]); } [Fact] public async Task BindActionArgumentsAsync_CallsValidator_IfModelBinderSucceeds() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.Parameters.Add( new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), }); var controllerContext = GetControllerContext(actionDescriptor); var factory = GetModelBinderFactory("Hello"); var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task BindActionArgumentsAsync_DoesNotCallValidator_IfModelBinderFails() { // Arrange Func method = foo => 1; var actionDescriptor = GetActionDescriptor(); actionDescriptor.Parameters.Add( new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), }); var controllerContext = GetControllerContext(actionDescriptor); var arguments = new Dictionary(StringComparer.Ordinal); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var factory = GetModelBinderFactory(binder.Object); var controller = new TestController(); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Fact] public async Task BindActionArgumentsAsync_CallsValidator_ForControllerProperties_IfModelBinderSucceeds() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = nameof(TestController.StringProperty), ParameterType = typeof(string), }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var factory = GetModelBinderFactory("Hello"); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task BindActionArgumentsAsync_DoesNotCallValidator_ForControllerProperties_IfModelBinderFails() { // Arrange Func method = foo => 1; var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = nameof(TestController.StringProperty), ParameterType = typeof(string), }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(TaskCache.CompletedTask); var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Fact] public async Task BindActionArgumentsAsync_SetsControllerProperties_ForReferenceTypes() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = nameof(TestController.StringProperty), BindingInfo = new BindingInfo(), ParameterType = typeof(string) }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var factory = GetModelBinderFactory("Hello"); var argumentBinder = GetArgumentBinder(factory); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal("Hello", controller.StringProperty); Assert.Equal(new List { "goodbye" }, controller.CollectionProperty); Assert.Null(controller.UntouchedProperty); } [Fact] public async Task BindActionArgumentsAsync_AddsToCollectionControllerProperties() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = nameof(TestController.CollectionProperty), BindingInfo = new BindingInfo(), ParameterType = typeof(ICollection), }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var expected = new List { "Hello", "World", "!!" }; var factory = GetModelBinderFactory(expected); var argumentBinder = GetArgumentBinder(factory); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal(expected, controller.CollectionProperty); Assert.Null(controller.StringProperty); Assert.Null(controller.UntouchedProperty); } [Theory] [InlineData(false)] [InlineData(true)] public async Task BindActionArgumentsAsync_DoesNotSetNullValues_ForNonNullableProperties(bool isModelSet) { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = nameof(TestController.NonNullableProperty), BindingInfo = new BindingInfo() { BindingSource = BindingSource.Custom }, ParameterType = typeof(int) }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var binder = new StubModelBinder(ModelBindingResult.Success(model: null)); var factory = GetModelBinderFactory(binder); var argumentBinder = GetArgumentBinder(factory); // Some non default value. controller.NonNullableProperty = -1; // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal(-1, controller.NonNullableProperty); } [Fact] public async Task BindActionArgumentsAsync_SetsNullValues_ForNullableProperties() { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = "NullableProperty", BindingInfo = new BindingInfo() { BindingSource = BindingSource.Custom }, ParameterType = typeof(int?) }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var binder = new StubModelBinder(ModelBindingResult.Success(model: null)); var factory = GetModelBinderFactory(binder); var argumentBinder = GetArgumentBinder(factory); // Some non default value. controller.NullableProperty = -1; // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Null(controller.NullableProperty); } // property name, property type, property accessor, input value, expected value public static TheoryData, object, object> SkippedPropertyData { get { return new TheoryData, object, object> { { nameof(TestController.ArrayProperty), typeof(string[]), controller => ((TestController)controller).ArrayProperty, new string[] { "hello", "world" }, new string[] { "goodbye" } }, { nameof(TestController.CollectionProperty), typeof(ICollection), controller => ((TestController)controller).CollectionProperty, null, new List { "goodbye" } }, { nameof(TestController.NonCollectionProperty), typeof(Person), controller => ((TestController)controller).NonCollectionProperty, new Person { Name = "Fred" }, new Person { Name = "Ginger" } }, { nameof(TestController.NullCollectionProperty), typeof(ICollection), controller => ((TestController)controller).NullCollectionProperty, new List { "hello", "world" }, null }, }; } } [Theory] [MemberData(nameof(SkippedPropertyData))] public async Task BindActionArgumentsAsync_SkipsReadOnlyControllerProperties( string propertyName, Type propertyType, Func propertyAccessor, object inputValue, object expectedValue) { // Arrange var actionDescriptor = GetActionDescriptor(); actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = propertyName, BindingInfo = new BindingInfo(), ParameterType = propertyType, }); var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var factory = GetModelBinderFactory(inputValue); var argumentBinder = GetArgumentBinder(factory); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal(expectedValue, propertyAccessor(controller)); Assert.Null(controller.StringProperty); Assert.Null(controller.UntouchedProperty); } [Fact] public async Task BindActionArgumentsAsync_SetsMultipleControllerProperties() { // Arrange var boundPropertyTypes = new Dictionary { { nameof(TestController.ArrayProperty), typeof(string[]) }, // Skipped { nameof(TestController.CollectionProperty), typeof(List) }, { nameof(TestController.NonCollectionProperty), typeof(Person) }, // Skipped { nameof(TestController.NullCollectionProperty), typeof(List) }, // Skipped { nameof(TestController.StringProperty), typeof(string) }, }; var inputPropertyValues = new Dictionary { { nameof(TestController.ArrayProperty), new string[] { "hello", "world" } }, { nameof(TestController.CollectionProperty), new List { "hello", "world" } }, { nameof(TestController.NonCollectionProperty), new Person { Name = "Fred" } }, { nameof(TestController.NullCollectionProperty), new List { "hello", "world" } }, { nameof(TestController.StringProperty), "Hello" }, }; var expectedPropertyValues = new Dictionary { { nameof(TestController.ArrayProperty), new string[] { "goodbye" } }, { nameof(TestController.CollectionProperty), new List { "hello", "world" } }, { nameof(TestController.NonCollectionProperty), new Person { Name = "Ginger" } }, { nameof(TestController.NullCollectionProperty), null }, { nameof(TestController.StringProperty), "Hello" }, }; var actionDescriptor = GetActionDescriptor(); foreach (var keyValuePair in boundPropertyTypes) { actionDescriptor.BoundProperties.Add( new ParameterDescriptor { Name = keyValuePair.Key, BindingInfo = new BindingInfo(), ParameterType = keyValuePair.Value, }); } var controllerContext = GetControllerContext(actionDescriptor); var controller = new TestController(); var arguments = new Dictionary(StringComparer.Ordinal); var binder = new StubModelBinder(bindingContext => { // BindingContext.ModelName will be string.Empty here. This is a 'fallback to empty prefix' // because the value providers have no data. object model; if (inputPropertyValues.TryGetValue(bindingContext.FieldName, out model)) { bindingContext.Result = ModelBindingResult.Success( model); } else { bindingContext.Result = ModelBindingResult.Failed(); } }); var factory = GetModelBinderFactory(binder); controllerContext.ValueProviderFactories.Add(new SimpleValueProviderFactory()); var argumentBinder = GetArgumentBinder(factory); // Act await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments); // Assert Assert.Equal(new string[] { "goodbye" }, controller.ArrayProperty); // Skipped Assert.Equal(new List { "hello", "world" }, controller.CollectionProperty); Assert.Equal(new Person { Name = "Ginger" }, controller.NonCollectionProperty); // Skipped Assert.Null(controller.NullCollectionProperty); // Skipped Assert.Null(controller.UntouchedProperty); // Not bound Assert.Equal("Hello", controller.StringProperty); } public static TheoryData BindModelAsyncData { get { var emptyBindingInfo = new BindingInfo(); var bindingInfoWithName = new BindingInfo { BinderModelName = "bindingInfoName", BinderType = typeof(Person), }; // parameterBindingInfo, metadataBinderModelName, parameterName, expectedBinderModelName return new TheoryData { // If the parameter name is not a prefix match, it is ignored. But name is required to create a // ModelBindingContext. { null, null, "parameterName", string.Empty }, { emptyBindingInfo, null, "parameterName", string.Empty }, { bindingInfoWithName, null, "parameterName", "bindingInfoName" }, { null, "modelBinderName", "parameterName", "modelBinderName" }, { null, null, "parameterName", string.Empty }, // Parameter's BindingInfo has highest precedence { bindingInfoWithName, "modelBinderName", "parameterName", "bindingInfoName" }, }; } } [Theory] [MemberData(nameof(BindModelAsyncData))] public async Task BindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixDoesNotMatch( BindingInfo parameterBindingInfo, string metadataBinderModelName, string parameterName, string expectedModelName) { // Arrange var metadataProvider = new TestModelMetadataProvider(); metadataProvider.ForType().BindingDetails(binding => { binding.BinderModelName = metadataBinderModelName; }); var metadata = metadataProvider.GetMetadataForType(typeof(Person)); var modelBinder = new Mock(); modelBinder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal); }) .Returns(TaskCache.CompletedTask); var parameterDescriptor = new ParameterDescriptor { BindingInfo = parameterBindingInfo, Name = parameterName, ParameterType = typeof(Person), }; var factory = new Mock(MockBehavior.Strict); factory .Setup(f => f.CreateBinder(It.IsAny())) .Callback((ModelBinderFactoryContext context) => { // Confirm expected data is passed through to ModelBindingFactory. Assert.Same(parameterDescriptor.BindingInfo, context.BindingInfo); Assert.Same(parameterDescriptor, context.CacheToken); Assert.Equal(metadata, context.Metadata); }) .Returns(modelBinder.Object); var argumentBinder = new ControllerArgumentBinder(metadataProvider, factory.Object, CreateMockValidator()); var controllerContext = GetControllerContext(); controllerContext.ActionDescriptor.Parameters.Add(parameterDescriptor); // Act & Assert await argumentBinder.BindModelAsync(parameterDescriptor, controllerContext); } [Fact] public async Task BindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixMatches() { // Arrange var expectedModelName = "expectedName"; var metadataProvider = new TestModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(Person)); var modelBinder = new Mock(); modelBinder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal); }) .Returns(TaskCache.CompletedTask); var parameterDescriptor = new ParameterDescriptor { Name = expectedModelName, ParameterType = typeof(Person), }; var factory = new Mock(MockBehavior.Strict); factory .Setup(f => f.CreateBinder(It.IsAny())) .Callback((ModelBinderFactoryContext context) => { // Confirm expected data is passed through to ModelBindingFactory. Assert.Null(context.BindingInfo); Assert.Same(parameterDescriptor, context.CacheToken); Assert.Equal(metadata, context.Metadata); }) .Returns(modelBinder.Object); var argumentBinder = new ControllerArgumentBinder(metadataProvider, factory.Object, CreateMockValidator()); var valueProvider = new SimpleValueProvider { { expectedModelName, new object() }, }; var valueProviderFactory = new SimpleValueProviderFactory(valueProvider); var controllerContext = GetControllerContext(); controllerContext.ActionDescriptor.Parameters.Add(parameterDescriptor); controllerContext.ValueProviderFactories.Insert(0, valueProviderFactory); // Act & Assert await argumentBinder.BindModelAsync(parameterDescriptor, controllerContext); } private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null) { var context = new ControllerContext() { ActionDescriptor = descriptor ?? GetActionDescriptor(), HttpContext = new DefaultHttpContext(), RouteData = new RouteData(), }; context.ValueProviderFactories.Add(new SimpleValueProviderFactory()); return context; } private static ControllerActionDescriptor GetActionDescriptor() { Func method = foo => 1; return new ControllerActionDescriptor { MethodInfo = method.GetMethodInfo(), ControllerTypeInfo = typeof(TestController).GetTypeInfo(), BoundProperties = new List(), Parameters = new List() }; } private static ModelBinderFactory GetModelBinderFactory(object model = null) { var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(mbc => { mbc.Result = ModelBindingResult.Success(model); return TaskCache.CompletedTask; }); return GetModelBinderFactory(binder.Object); } private static ModelBinderFactory GetModelBinderFactory(IModelBinder binder) { var provider = new Mock(); provider .Setup(p => p.GetBinder(It.IsAny())) .Returns(binder); return TestModelBinderFactory.Create(provider.Object); } private static ControllerArgumentBinder GetArgumentBinder( IModelBinderFactory factory = null, IObjectModelValidator validator = null) { if (validator == null) { validator = CreateMockValidator(); } if (factory == null) { factory = TestModelBinderFactory.CreateDefault(); } return new ControllerArgumentBinder( TestModelMetadataProvider.CreateDefaultProvider(), factory, validator); } private static IObjectModelValidator CreateMockValidator() { var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); return mockValidator.Object; } // No need for bind-related attributes on properties in this controller class. Properties are added directly // to the BoundProperties collection, bypassing usual requirements. private class TestController { public string UntouchedProperty { get; set; } public string[] ArrayProperty { get; } = new string[] { "goodbye" }; public ICollection CollectionProperty { get; } = new List { "goodbye" }; public Person NonCollectionProperty { get; } = new Person { Name = "Ginger" }; public ICollection NullCollectionProperty { get; private set; } public string StringProperty { get; set; } public int NonNullableProperty { get; set; } public int? NullableProperty { get; set; } } private class Person : IEquatable, IEquatable { public string Name { get; set; } public bool Equals(Person other) { return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal); } bool IEquatable.Equals(object obj) { return Equals(obj as Person); } } private class CustomBindingSourceAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Custom; } } } private class ValueProviderMetadataAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Query; } } } } }