// 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.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); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert Assert.Empty(result); } [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); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert Assert.Empty(result); } [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(string.Empty, value); }) .Returns(TaskCache.CompletedTask); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory); var controllerContext = GetControllerContext(actionDescriptor); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert Assert.Equal(1, result.Count); Assert.Equal(value, result["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(), It.IsAny())); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), 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 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(), It.IsAny())); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), 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 mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var factory = GetModelBinderFactory("Hello"); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), 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 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(), It.IsAny())); var factory = GetModelBinderFactory(binder.Object); var argumentBinder = GetArgumentBinder(factory, mockValidator.Object); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController()); // Assert mockValidator .Verify(o => o.Validate( It.IsAny(), 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 factory = GetModelBinderFactory("Hello"); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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 expected = new List { "Hello", "World", "!!" }; var factory = GetModelBinderFactory(expected); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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 binder = new StubModelBinder(ModelBindingResult.Success(string.Empty, model: null)); var factory = GetModelBinderFactory(binder); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Some non default value. controller.NonNullableProperty = -1; // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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 binder = new StubModelBinder(ModelBindingResult.Success(key: string.Empty, model: null)); var factory = GetModelBinderFactory(binder); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Some non default value. controller.NullableProperty = -1; // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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 factory = GetModelBinderFactory(inputValue); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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 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(bindingContext.ModelName, model); } else { bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName); } }); var factory = GetModelBinderFactory(binder); controllerContext.ValueProviders.Add(new SimpleValueProvider()); var argumentBinder = GetArgumentBinder(factory); var controller = new TestController(); // Act var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller); // 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); } private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null) { var context = new ControllerContext() { ActionDescriptor = descriptor ?? GetActionDescriptor(), HttpContext = new DefaultHttpContext(), RouteData = new RouteData(), }; context.ValueProviders.Add(new SimpleValueProvider()); 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(string.Empty, 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(), 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; } } } } }