// 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.ComponentModel; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { public class InferParameterBindingInfoConventionTest { [Fact] public void Apply_DoesNotInferBindingSourceForParametersWithBindingInfo() { // Arrange var actionName = nameof(ParameterWithBindingInfo.Action); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterWithBindingInfo), actionName); // Act convention.Apply(action.Controller); // Assert var parameterModel = Assert.Single(action.Parameters); Assert.NotNull(parameterModel.BindingInfo); Assert.Same(BindingSource.Custom, parameterModel.BindingInfo.BindingSource); } [Fact] public void InferParameterBindingSources_Throws_IfMultipleParametersAreInferredAsBodyBound() { // Arrange var actionName = nameof(MultipleFromBodyController.MultipleInferred); var expected = $@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " + "has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" + Environment.NewLine + "TestModel a" + Environment.NewLine + "Car b"; var convention = GetConvention(); var action = GetActionModel(typeof(MultipleFromBodyController), actionName); // Act & Assert var ex = Assert.Throws(() => convention.InferParameterBindingSources(action)); Assert.Equal(expected, ex.Message); } [Fact] public void InferParameterBindingSources_Throws_IfMultipleParametersAreInferredOrSpecifiedAsBodyBound() { // Arrange var actionName = nameof(MultipleFromBodyController.InferredAndSpecified); var expected = $@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " + "has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" + Environment.NewLine + "TestModel a" + Environment.NewLine + "int b"; var convention = GetConvention(); var action = GetActionModel(typeof(MultipleFromBodyController), actionName); // Act & Assert var ex = Assert.Throws(() => convention.InferParameterBindingSources(action)); Assert.Equal(expected, ex.Message); } [Fact] public void InferParameterBindingSources_Throws_IfMultipleParametersAreFromBody() { // Arrange var actionName = nameof(MultipleFromBodyController.MultipleSpecified); var expected = $@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " + "has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" + Environment.NewLine + "decimal a" + Environment.NewLine + "int b"; var convention = GetConvention(); var action = GetActionModel(typeof(MultipleFromBodyController), actionName); // Act & Assert var ex = Assert.Throws(() => convention.InferParameterBindingSources(action)); Assert.Equal(expected, ex.Message); } [Fact] public void Apply_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinder_AndExplicitName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ModelBinderOnParameterController.ModelBinderAttributeWithExplicitModelName); var convention = GetConvention(); var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Equal("top", bindingInfo.BinderModelName); } [Fact] public void Apply_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinderType() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ModelBinderOnParameterController.ModelBinderType); var convention = GetConvention(); var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Custom, bindingInfo.BindingSource); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void OnProvidersExecuting_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinderType_AndExplicitModelName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ModelBinderOnParameterController.ModelBinderTypeWithExplicitModelName); var convention = GetConvention(); var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Custom, bindingInfo.BindingSource); Assert.Equal("foo", bindingInfo.BinderModelName); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsSimpleToken() { // Arrange var actionName = nameof(ParameterBindingController.SimpleRouteToken); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsOptionalToken() { // Arrange var actionName = nameof(ParameterBindingController.OptionalRouteToken); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsConstrainedToken() { // Arrange var actionName = nameof(ParameterBindingController.ConstrainedRouteToken); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInAbsoluteRoute() { // Arrange var actionName = nameof(ParameterBindingController.AbsoluteRoute); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAnyRoutes_MulitpleRoutes() { // Arrange var actionName = nameof(ParameterBindingController.ParameterInMultipleRoutes); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAnyRoute() { // Arrange var actionName = nameof(ParameterBindingController.ParameterNotInAllRoutes); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInControllerRoute() { // Arrange var actionName = nameof(ParameterInController.ActionWithoutRoute); var parameter = GetParameterModel(typeof(ParameterInController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInControllerRoute_AndActionHasRoute() { // Arrange var actionName = nameof(ParameterInController.ActionWithRoute); var parameter = GetParameterModel(typeof(ParameterInController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAllActionRoutes() { // Arrange var actionName = nameof(ParameterInController.MultipleRoute); var parameter = GetParameterModel(typeof(ParameterInController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_DoesNotReturnPath_IfActionRouteOverridesControllerRoute() { // Arrange var actionName = nameof(ParameterInController.AbsoluteRoute); var parameter = GetParameterModel(typeof(ParameterInController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Query, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterPresentInNonOverriddenControllerRoute() { // Arrange var actionName = nameof(ParameterInController.MultipleRouteWithOverride); var parameter = GetParameterModel(typeof(ParameterInController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterExistsInRoute_OnControllersWithoutSelectors() { // Arrange var actionName = nameof(ParameterBindingNoRoutesOnController.SimpleRoute); var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_ReturnsPath_IfParameterExistsInAllRoutes_OnControllersWithoutSelectors() { // Arrange var actionName = nameof(ParameterBindingNoRoutesOnController.ParameterInMultipleRoutes); var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Path, result); } [Fact] public void InferBindingSourceForParameter_DoesNotReturnPath_IfNeitherActionNorControllerHasTemplate() { // Arrange var actionName = nameof(ParameterBindingNoRoutesOnController.NoRouteTemplate); var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Query, result); } [Fact] public void InferBindingSourceForParameter_ReturnsBodyForComplexTypes() { // Arrange var actionName = nameof(ParameterBindingController.ComplexTypeModel); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Body, result); } [Fact] public void InferParameterBindingSources_SetsCorrectBindingSourceForComplexTypesWithCancellationToken() { // Arrange var actionName = nameof(ParameterBindingController.ComplexTypeModelWithCancellationToken); // Use the default set of ModelMetadataProviders so we get metadata details for CancellationToken. var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); var controllerModel = Assert.Single(context.Result.Controllers); var actionModel = Assert.Single(controllerModel.Actions, m => m.ActionName == actionName); var convention = GetConvention(); // Act convention.InferParameterBindingSources(actionModel); // Assert var model = GetParameterModel(actionModel); Assert.Same(BindingSource.Body, model.BindingInfo.BindingSource); var cancellationToken = GetParameterModel(actionModel); Assert.Same(BindingSource.Special, cancellationToken.BindingInfo.BindingSource); } [Fact] public void InferBindingSourceForParameter_ReturnsBodyForSimpleTypes() { // Arrange var actionName = nameof(ParameterBindingController.SimpleTypeModel); var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); var convention = GetConvention(); // Act var result = convention.InferBindingSourceForParameter(parameter); // Assert Assert.Same(BindingSource.Query, result); } [Fact] public void PreservesBindingSourceInference_ForFromQueryParameter_WithDefaultName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQuery); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromQueryParameter_WithCustomName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryWithCustomName); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Equal("top", bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromQueryParameterOnComplexType_WithDefaultName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryOnComplexType); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); } [Fact] public void PreservesBindingSourceInference_ForFromQueryParameterOnComplexType_WithCustomName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryOnComplexTypeWithCustomName); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Equal("gps", bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromQueryParameterOnCollectionType() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryOnCollectionType); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromQueryOnArrayType() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryOnArrayType); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_FromQueryOnArrayTypeWithCustomName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromQueryOnArrayTypeWithCustomName); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Equal("ids", bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromRouteParameter_WithDefaultName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromRoute); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Path, bindingInfo.BindingSource); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromRouteParameter_WithCustomName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromRouteWithCustomName); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Path, bindingInfo.BindingSource); Assert.Equal("top", bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForFromRouteParameterOnComplexType_WithDefaultName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromRouteOnComplexType); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Path, bindingInfo.BindingSource); } [Fact] public void PreservesBindingSourceInference_ForFromRouteParameterOnComplexType_WithCustomName() { // Arrange var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.FromRouteOnComplexTypeWithCustomName); var convention = GetConvention(); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Path, bindingInfo.BindingSource); Assert.Equal("gps", bindingInfo.BinderModelName); } [Fact] public void PreservesBindingSourceInference_ForParameterWithRequestPredicateAndPropertyFilterProvider() { // Arrange var expectedPredicate = CustomRequestPredicateAndPropertyFilterProviderAttribute.RequestPredicateStatic; var expectedPropertyFilter = CustomRequestPredicateAndPropertyFilterProviderAttribute.PropertyFilterStatic; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var actionName = nameof(ParameterBindingController.ParameterWithRequestPredicateProvider); var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider); var convention = GetConvention(); // Act convention.Apply(action.Controller); // Assert var parameter = Assert.Single(action.Parameters); var bindingInfo = parameter.BindingInfo; Assert.NotNull(bindingInfo); Assert.Same(BindingSource.Query, bindingInfo.BindingSource); Assert.Same(expectedPredicate, bindingInfo.RequestPredicate); Assert.Same(expectedPropertyFilter, bindingInfo.PropertyFilterProvider.PropertyFilter); Assert.Null(bindingInfo.BinderModelName); } [Fact] public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider() { // Arrange var controller = GetControllerModel(typeof(ControllerWithBoundProperty)); var convention = GetConvention(); // Act convention.InferBoundPropertyModelPrefixes(controller); // Assert var property = Assert.Single(controller.ControllerProperties, p => p.Name == nameof(ControllerWithBoundProperty.TestProperty)); Assert.Equal(string.Empty, property.BindingInfo.BinderModelName); } [Fact] public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForCollectionTypeFromValueProvider() { // Arrange var controller = GetControllerModel(typeof(ControllerWithBoundCollectionProperty)); var convention = GetConvention(); // Act convention.InferBoundPropertyModelPrefixes(controller); // Assert var property = Assert.Single(controller.ControllerProperties); Assert.Null(property.BindingInfo.BinderModelName); } [Fact] public void InferParameterModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider() { // Arrange var action = GetActionModel(typeof(ControllerWithBoundProperty), nameof(ControllerWithBoundProperty.SomeAction)); var convention = GetConvention(); // Act convention.InferParameterModelPrefixes(action); // Assert var parameter = Assert.Single(action.Parameters); Assert.Equal(string.Empty, parameter.BindingInfo.BinderModelName); } [Fact] public void InferParameterModelPrefixes_DoesNotSetModelPrefix_ForFormFileParametersAnnotatedWithFromForm() { // Arrange var action = GetActionModel( typeof(ParameterBindingController), nameof(ParameterBindingController.FromFormFormFileParameters), TestModelMetadataProvider.CreateDefaultProvider()); var convention = GetConvention(); // Act convention.InferParameterModelPrefixes(action); // Assert Assert.Collection( action.Parameters, parameter => { Assert.Equal("p1", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }, parameter => { Assert.Equal("p2", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }, parameter => { Assert.Equal("p3", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }); } [Fact] public void InferParameterModelPrefixes_DoesNotSetModelPrefix_ForFormFileParameters() { // Arrange var action = GetActionModel( typeof(ParameterBindingController), nameof(ParameterBindingController.FormFileParameters), TestModelMetadataProvider.CreateDefaultProvider()); var convention = GetConvention(); // Act convention.InferParameterModelPrefixes(action); // Assert Assert.Collection( action.Parameters, parameter => { Assert.Equal("p1", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }, parameter => { Assert.Equal("p2", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }, parameter => { Assert.Equal("p3", parameter.Name); Assert.Null(parameter.BindingInfo.BinderModelName); }); } [Fact] public void InferBoundPropertyModelPrefixes_DoesNotSetModelPrefix_ForFormFileCollectionPropertiesAnnotatedWithFromForm() { // Arrange var controller = GetControllerModel(typeof(ControllerWithBoundProperty)); var convention = GetConvention(); // Act convention.InferBoundPropertyModelPrefixes(controller); // Assert var parameter = Assert.Single(controller.ControllerProperties, p => p.Name == nameof(ControllerWithBoundProperty.Files)); Assert.Null(parameter.BindingInfo.BinderModelName); } private static InferParameterBindingInfoConvention GetConvention( IModelMetadataProvider modelMetadataProvider = null) { var loggerFactory = NullLoggerFactory.Instance; modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); return new InferParameterBindingInfoConvention(modelMetadataProvider); } private static ApplicationModelProviderContext GetContext( Type type, IModelMetadataProvider modelMetadataProvider = null) { var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() }); var mvcOptions = Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true }); modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); var convention = new DefaultApplicationModelProvider(mvcOptions, modelMetadataProvider); convention.OnProvidersExecuting(context); return context; } private static ControllerModel GetControllerModel( Type controllerType, IModelMetadataProvider modelMetadataProvider = null) { var context = GetContext(controllerType, modelMetadataProvider); return Assert.Single(context.Result.Controllers); } private static ActionModel GetActionModel( Type controllerType, string actionName, IModelMetadataProvider modelMetadataProvider = null) { var context = GetContext(controllerType, modelMetadataProvider); var controller = Assert.Single(context.Result.Controllers); return Assert.Single(controller.Actions, m => m.ActionName == actionName); } private static ParameterModel GetParameterModel(Type controllerType, string actionName) { var action = GetActionModel(controllerType, actionName); return Assert.Single(action.Parameters); } private static ParameterModel GetParameterModel(ActionModel action) { return Assert.Single(action.Parameters.Where(x => typeof(T).IsAssignableFrom(x.ParameterType))); } [ApiController] [Route("[controller]/[action]")] private class ParameterBindingController { [HttpGet("{parameter}")] public IActionResult ActionWithBoundParameter([FromBody] object parameter) => null; [HttpGet("{id}")] public IActionResult SimpleRouteToken(int id) => null; [HttpPost("optional/{id?}")] public IActionResult OptionalRouteToken(int id) => null; [HttpDelete("delete-by-status/{status:int?}")] public IActionResult ConstrainedRouteToken(object status) => null; [HttpPut("/absolute-route/{status:int}")] public IActionResult AbsoluteRoute(object status) => null; [HttpPost("multiple/{id}")] [HttpPut("multiple/{id}")] public IActionResult ParameterInMultipleRoutes(int id) => null; [HttpPatch("patchroute")] [HttpPost("multiple/{id}")] [HttpPut("multiple/{id}")] public IActionResult ParameterNotInAllRoutes(int id) => null; [HttpPut("put-action/{id}")] public IActionResult ComplexTypeModel(TestModel model) => null; [HttpPut("put-action/{id}")] public IActionResult SimpleTypeModel(ConvertibleFromString model) => null; [HttpPost("form-file")] public IActionResult FormFileParameter(IFormFile formFile) => null; [HttpPost("form-file-collection")] public IActionResult FormFileCollectionParameter(IFormFileCollection formFiles) => null; [HttpPost("form-file-sequence")] public IActionResult FormFileSequenceParameter(IFormFile[] formFiles) => null; [HttpPost] public IActionResult FromFormParameter([FromForm] string parameter) => null; [HttpPost] [Consumes("application/json")] public IActionResult ActionWithConsumesAttribute([FromForm] string parameter) => null; [HttpPut("cancellation")] public IActionResult ComplexTypeModelWithCancellationToken(TestModel model, CancellationToken cancellationToken) => null; [HttpGet("parameter-with-model-binder-attribute")] public IActionResult ModelBinderAttribute([ModelBinder(Name = "top")] int value) => null; [HttpGet("parameter-with-fromquery")] public IActionResult FromQuery([FromQuery] int value) => null; [HttpGet("parameter-with-fromquery-and-customname")] public IActionResult FromQueryWithCustomName([FromQuery(Name = "top")] int value) => null; [HttpGet("parameter-with-fromquery-on-complextype")] public IActionResult FromQueryOnComplexType([FromQuery] GpsCoordinates gpsCoordinates) => null; [HttpGet("parameter-with-fromquery-on-complextype-and-customname")] public IActionResult FromQueryOnComplexTypeWithCustomName([FromQuery(Name = "gps")] GpsCoordinates gpsCoordinates) => null; [HttpGet("parameter-with-fromquery-on-collection-type")] public IActionResult FromQueryOnCollectionType([FromQuery] ICollection value) => null; [HttpGet("parameter-with-fromquery-on-array-type")] public IActionResult FromQueryOnArrayType([FromQuery] int[] value) => null; [HttpGet("parameter-with-fromquery-on-array-type-customname")] public IActionResult FromQueryOnArrayTypeWithCustomName([FromQuery(Name = "ids")] int[] value) => null; [HttpGet("parameter-with-fromroute")] public IActionResult FromRoute([FromRoute] int value) => null; [HttpGet("parameter-with-fromroute-and-customname")] public IActionResult FromRouteWithCustomName([FromRoute(Name = "top")] int value) => null; [HttpGet("parameter-with-fromroute-on-complextype")] public IActionResult FromRouteOnComplexType([FromRoute] GpsCoordinates gpsCoordinates) => null; [HttpGet("parameter-with-fromroute-on-complextype-and-customname")] public IActionResult FromRouteOnComplexTypeWithCustomName([FromRoute(Name = "gps")] GpsCoordinates gpsCoordinates) => null; [HttpGet] public IActionResult ParameterWithRequestPredicateProvider([CustomRequestPredicateAndPropertyFilterProvider] int value) => null; public IActionResult FromFormFormFileParameters([FromForm] IFormFile p1, [FromForm] IFormFile[] p2, [FromForm] IFormFileCollection p3) => null; public IActionResult FormFileParameters(IFormFile p1, IFormFile[] p2, IFormFileCollection p3) => null; } [ApiController] [Route("[controller]/[action]")] private class ModelBinderOnParameterController { [HttpGet] public IActionResult ModelBinderAttributeWithExplicitModelName([ModelBinder(Name = "top")] int value) => null; [HttpGet] public IActionResult ModelBinderType([ModelBinder(typeof(TestModelBinder))] string name) => null; [HttpGet] public IActionResult ModelBinderTypeWithExplicitModelName([ModelBinder(typeof(TestModelBinder), Name = "foo")] string name) => null; } [ApiController] [Route("/route1/[controller]/[action]/{id}")] [Route("/route2/[controller]/[action]/{id?}")] private class ParameterInController { [HttpGet] public IActionResult ActionWithoutRoute(int id) => null; [HttpGet("stuff/{status}")] public IActionResult ActionWithRoute(int id) => null; [HttpGet("/absolute-route")] public IActionResult AbsoluteRoute(int id) => null; [HttpPut] [HttpPost("stuff/{status}")] public IActionResult MultipleRoute(int id) => null; [HttpPut] [HttpPost("~/stuff/{status}")] public IActionResult MultipleRouteWithOverride(int id) => null; } [ApiController] private class ParameterBindingNoRoutesOnController { [HttpGet("{parameter}")] public IActionResult SimpleRoute(int parameter) => null; [HttpGet] public IActionResult NoRouteTemplate(int id) => null; [HttpPost("multiple/{id}")] [HttpPut("multiple/{id}")] public IActionResult ParameterInMultipleRoutes(int id) => null; } private class TestModel { } [TypeConverter(typeof(ConvertibleFromStringConverter))] private class ConvertibleFromString { } private class ConvertibleFromStringConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); } private class CustomRequestPredicateAndPropertyFilterProviderAttribute : Attribute, IRequestPredicateProvider, IPropertyFilterProvider { public static Func RequestPredicateStatic => (c) => true; public static Func PropertyFilterStatic => (c) => true; public Func RequestPredicate => RequestPredicateStatic; public Func PropertyFilter => PropertyFilterStatic; } private class GpsCoordinates { public long Latitude { get; set; } public long Longitude { get; set; } } private class TestModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { throw new NotImplementedException(); } } private class ControllerWithBoundProperty { [FromQuery] public TestModel TestProperty { get; set; } [FromForm] public IList Files { get; set; } public IActionResult SomeAction([FromQuery] TestModel test) => null; } private class ControllerWithBoundCollectionProperty { [FromQuery] public List TestProperty { get; set; } public IActionResult SomeAction([FromQuery] List test) => null; } private class Car { } private class MultipleFromBodyController { public IActionResult MultipleInferred(TestModel a, Car b) => null; public IActionResult InferredAndSpecified(TestModel a, [FromBody] int b) => null; public IActionResult MultipleSpecified([FromBody] decimal a, [FromBody] int b) => null; } private class ParameterWithBindingInfo { [HttpGet("test")] public IActionResult Action([ModelBinder(typeof(object))] Car car) => null; } } }