// 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.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.WebApiCompatShim; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace System.Web.Http { public class ApiControllerActionDiscoveryTest { [Fact] public void GetActions_ApiControllerWithControllerSuffix_IsController() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.ProductsController).GetTypeInfo(); var actions = results.Where(ad => ad.ControllerTypeInfo == controllerType).ToArray(); Assert.NotEmpty(actions); } [Fact] public void GetActions_ApiControllerWithoutControllerSuffix_IsNotController() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.Blog).GetTypeInfo(); var actions = results.Where(ad => ad.ControllerTypeInfo == controllerType); Assert.Equal(2, actions.Count()); } [Fact] public void GetActions_CreatesNamedAndUnnamedAction() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.MethodInfo.Name == "GetAll") .ToArray(); Assert.Equal(2, actions.Length); var action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "GetAll")); Assert.Equal( new string[] { "GET" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value))); Assert.Equal( new string[] { "GET" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); } [Fact] public void GetActions_CreatesNamedAndUnnamedAction_DefaultVerbIsPost() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.MethodInfo.Name == "Edit") .ToArray(); Assert.Equal(2, actions.Length); var action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "Edit")); Assert.Equal( new string[] { "POST" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value))); Assert.Equal( new string[] { "POST" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); } [Fact] public void GetActions_CreatesNamedAndUnnamedAction_RespectsVerbAttribute() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.MethodInfo.Name == "Delete") .ToArray(); Assert.Equal(2, actions.Length); var action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "Delete")); Assert.Equal( new string[] { "PUT" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value))); Assert.Equal( new string[] { "PUT" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); } // The method name is used to infer a verb, not the action name [Fact] public void GetActions_CreatesNamedAndUnnamedAction_VerbBasedOnMethodName() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.MethodInfo.Name == "Options") .ToArray(); Assert.Equal(2, actions.Length); var action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && rc.Value == "GetOptions")); Assert.Equal( new string[] { "OPTIONS" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); action = Assert.Single( actions, a => a.RouteValues.Any(rc => rc.Key == "action" && string.IsNullOrEmpty(rc.Value))); Assert.Equal( new string[] { "OPTIONS" }, Assert.Single(action.ActionConstraints.OfType()).HttpMethods); } [Fact] public void GetActions_AllWebApiActionsAreOverloaded() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { Assert.Single(action.ActionConstraints, c => c is OverloadActionConstraint); } } [Fact] public void GetActions_AllWebApiActionsAreInWebApiArea() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { Assert.Single(action.RouteValues, c => c.Key == "area" && c.Value== "api"); } } [Fact] public void GetActions_Parameters_SimpleTypeFromUriByDefault() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.ActionName == "Get") .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { var parameter = Assert.Single(action.Parameters); Assert.Equal((new FromUriAttribute()).BindingSource, parameter.BindingInfo.BindingSource); var optionalParameters = (HashSet)action.Properties["OptionalParameters"]; Assert.DoesNotContain(parameter.Name, optionalParameters); } } [Fact] public void GetActions_Parameters_ComplexTypeFromBodyByDefault() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.ActionName == "Put") .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { var parameter = Assert.Single(action.Parameters); Assert.Equal(BindingSource.Body, parameter.BindingInfo.BindingSource); } } [Fact] public void GetActions_Parameters_WithBindingSource() { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.ActionName == "Post") .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { var parameter = Assert.Single(action.Parameters); Assert.Null(parameter.BindingInfo.BindingSource); } } [Theory] [InlineData(nameof(TestControllers.EventsController.GetWithId))] [InlineData(nameof(TestControllers.EventsController.GetWithEmployee))] public void GetActions_Parameters_ImplicitOptional(string name) { // Arrange var provider = CreateProvider(); // Act var context = new ActionDescriptorProviderContext(); Invoke(provider, context); var results = context.Results.Cast(); // Assert var controllerType = typeof(TestControllers.EventsController).GetTypeInfo(); var actions = results .Where(ad => ad.ControllerTypeInfo == controllerType) .Where(ad => ad.ActionName == name) .ToArray(); Assert.NotEmpty(actions); foreach (var action in actions) { var parameter = Assert.Single(action.Parameters); Assert.Equal((new FromUriAttribute()).BindingSource, parameter.BindingInfo.BindingSource); var optionalParameters = (HashSet)action.Properties["OptionalParameters"]; Assert.Contains(parameter.Name, optionalParameters); } } private ControllerActionDescriptorProvider CreateProvider() { var manager = GetApplicationManager(GetType().GetTypeInfo().Assembly.DefinedTypes.ToArray()); var options = new MvcOptions(); var setup = new WebApiCompatShimOptionsSetup(); setup.Configure(options); var optionsAccessor = new Mock>(); optionsAccessor .SetupGet(o => o.Value) .Returns(options); var authorizationOptionsAccessor = new Mock>(); authorizationOptionsAccessor .SetupGet(o => o.Value) .Returns(new AuthorizationOptions()); var modelProvider = new DefaultApplicationModelProvider(optionsAccessor.Object); var provider = new ControllerActionDescriptorProvider( manager, new[] { modelProvider }, optionsAccessor.Object); return provider; } private void Invoke(ControllerActionDescriptorProvider provider, ActionDescriptorProviderContext context) { provider.OnProvidersExecuting(context); provider.OnProvidersExecuted(context); } private static ApplicationPartManager GetApplicationManager(params TypeInfo[] controllerTypes) { var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new TestPart(controllerTypes)); manager.FeatureProviders.Add(new TestProvider()); manager.FeatureProviders.Add(new NamespaceFilteredControllersFeatureProvider()); return manager; } private class TestPart : ApplicationPart, IApplicationPartTypeProvider { public TestPart(IEnumerable types) { Types = types; } public override string Name => "Test"; public IEnumerable Types { get; } } private class TestProvider : IApplicationFeatureProvider { public void PopulateFeature(IEnumerable parts, ControllerFeature feature) { foreach (var type in parts.OfType().SelectMany(t => t.Types)) { feature.Controllers.Add(type); } } } private class NamespaceFilteredControllersFeatureProvider : IApplicationFeatureProvider { public void PopulateFeature(IEnumerable parts, ControllerFeature feature) { var controllers = feature.Controllers.ToList(); foreach (var controller in controllers) { if (controller.Namespace != "System.Web.Http.TestControllers") { feature.Controllers.Remove(controller); } } } } } } // These need to be public top-level classes to test discovery end-to-end. Don't reuse // these outside of this test. namespace System.Web.Http.TestControllers { public class ProductsController : ApiController { public IActionResult GetAll() { return null; } } // Not a controller, because there's no controller suffix public class Blog : ApiController { public IActionResult GetBlogPosts() { return null; } } public class StoreController : ApiController { public IActionResult GetAll() { return null; } public IActionResult Edit(int id) { return null; } [HttpPut] public IActionResult Delete(int id) { return null; } [ActionName("GetOptions")] public IActionResult Options() { return null; } } public class EmployeesController : ApiController { public IActionResult Get(int id) { return null; } public IActionResult Put(Employee employee) { return null; } public IActionResult Post([ModelBinder] Employee employee) { return null; } } public class Employee { } public class EventsController : ApiController { public IActionResult GetWithId(int id = 0) { return null; } public IActionResult GetWithEmployee([FromUri] Employee e = null) { return null; } } }