// 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.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal { public class DefaultApplicationModelProviderTest { [Fact] public void CreateControllerModel_DerivedFromControllerClass_HasFilter() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(StoreController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert var filter = Assert.Single(model.Filters); Assert.IsType(filter); } [Fact] public void OnProvidersExecuting_AddsControllerProperties() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ModelBinderController).GetTypeInfo(); var context = new ApplicationModelProviderContext(new[] { typeInfo }); // Act builder.OnProvidersExecuting(context); // Assert var model = Assert.Single(context.Result.Controllers); Assert.Equal(2, model.ControllerProperties.Count); Assert.Equal("Bound", model.ControllerProperties[0].PropertyName); Assert.Equal(BindingSource.Query, model.ControllerProperties[0].BindingInfo.BindingSource); Assert.NotNull(model.ControllerProperties[0].Controller); var attribute = Assert.Single(model.ControllerProperties[0].Attributes); Assert.IsType(attribute); } // This class has a filter attribute, but doesn't implement any filter interfaces, // so ControllerFilter is not present. [Fact] public void CreateControllerModel_ClassWithoutFilterInterfaces_HasNoControllerFilter() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoFiltersController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert var filter = Assert.Single(model.Filters); Assert.IsType(filter); } [Fact] public void CreateControllerModel_ClassWithFilterInterfaces_HasFilter() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(SomeFiltersController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert Assert.Single(model.Filters, f => f is ControllerActionFilter); Assert.Single(model.Filters, f => f is ControllerResultFilter); } [Fact] public void CreateControllerModel_ClassWithFilterInterfaces_UnsupportedType() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(UnsupportedFiltersController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert Assert.Empty(model.Filters); } [Fact] public void CreateControllerModel_ClassWithInheritedRoutes() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedClassInheritingRoutesController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert var attributeRoutes = GetAttributeRoutes(model.Selectors); Assert.Equal(2, attributeRoutes.Count); Assert.Equal(2, model.Attributes.Count); var route = Assert.Single(attributeRoutes, r => r.Template == "A"); Assert.Contains(route.Attribute, model.Attributes); route = Assert.Single(attributeRoutes, r => r.Template == "B"); Assert.Contains(route.Attribute, model.Attributes); } [Fact] public void CreateControllerModel_ClassWithHiddenInheritedRoutes() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedClassHidingRoutesController).GetTypeInfo(); // Act var model = builder.CreateControllerModel(typeInfo); // Assert var attributeRoutes = GetAttributeRoutes(model.Selectors); Assert.Equal(2, attributeRoutes.Count); Assert.Equal(2, model.Attributes.Count); var route = Assert.Single(attributeRoutes, r => r.Template == "C"); Assert.Contains(route.Attribute, model.Attributes); route = Assert.Single(attributeRoutes, r => r.Template == "D"); Assert.Contains(route.Attribute, model.Attributes); } [Theory] [InlineData("GetFromDerived", true)] [InlineData("NewMethod", true)] // "NewMethod" is a public method declared with keyword "new". [InlineData("GetFromBase", true)] public void IsAction_WithInheritedMethods(string methodName, bool expected) { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod(methodName); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.Equal(expected, isValid); } [Fact] public void IsAction_OverridenMethodControllerClass() { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(BaseController).GetMethod(nameof(BaseController.Redirect)); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(BaseController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void IsAction_PrivateMethod_FromUserDefinedController() { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod( "PrivateMethod", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void IsAction_OperatorOverloadingMethod_FromOperatorOverloadingController() { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(OperatorOverloadingController).GetMethod("op_Addition"); Assert.NotNull(method); Assert.True(method.IsSpecialName); // Act var isValid = builder.IsAction(typeof(OperatorOverloadingController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void IsAction_GenericMethod_FromUserDefinedController() { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod("GenericMethod"); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void IsAction_OverridenNonActionMethod() { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod("OverridenNonActionMethod"); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Theory] [InlineData("Equals")] [InlineData("GetHashCode")] [InlineData("MemberwiseClone")] [InlineData("ToString")] public void IsAction_OverriddenMethodsFromObjectClass(string methodName) { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod( methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void IsAction_DerivedControllerIDisposableDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedController).GetTypeInfo(); var methodInfo = typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0]; var method = typeInfo.AsType().GetMethods().SingleOrDefault(m => (m == methodInfo)); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.False(isValid); } [Fact] public void IsAction_DerivedControllerDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedController).GetTypeInfo(); var methodInfo = typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0]; var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose") && m != methodInfo); Assert.NotEmpty(methods); foreach (var method in methods) { // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.True(isValid); } } [Fact] public void IsAction_OverriddenDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedOverriddenDisposeController).GetTypeInfo(); var method = typeInfo.GetDeclaredMethods("Dispose").SingleOrDefault(); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.False(isValid); } [Fact] public void IsAction_NewDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedNewDisposeController).GetTypeInfo(); var method = typeInfo.GetDeclaredMethods("Dispose").SingleOrDefault(); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.True(isValid); } [Fact] public void IsAction_PocoControllerIDisposableDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(IDisposablePocoController).GetTypeInfo(); var methodInfo = typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0]; var method = typeInfo.AsType().GetMethods().SingleOrDefault(m => (m == methodInfo)); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.False(isValid); } [Fact] public void IsAction_PocoControllerDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(IDisposablePocoController).GetTypeInfo(); var methodInfo = typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0]; var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose") && m != methodInfo); Assert.NotEmpty(methods); foreach (var method in methods) { // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.True(isValid); } } [Fact] public void IsAction_SimplePocoControllerDisposeMethod() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(SimplePocoController).GetTypeInfo(); var methods = typeInfo.AsType().GetMethods().Where(m => m.Name.Equals("Dispose")); Assert.NotEmpty(methods); foreach (var method in methods) { // Act var isValid = builder.IsAction(typeInfo, method); // Assert Assert.True(isValid); } } [Theory] [InlineData("StaticMethod")] [InlineData("ProtectedStaticMethod")] [InlineData("PrivateStaticMethod")] public void IsAction_StaticMethods(string methodName) { // Arrange var builder = new TestApplicationModelProvider(); var method = typeof(DerivedController).GetMethod( methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); Assert.NotNull(method); // Act var isValid = builder.IsAction(typeof(DerivedController).GetTypeInfo(), method); // Assert Assert.False(isValid); } [Fact] public void CreateActionModel_ConventionallyRoutedAction_WithoutHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo(); var actionName = nameof(ConventionallyRoutedController.Edit); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Equal(actionName, action.ActionName); Assert.Empty(action.Attributes); Assert.Single(action.Selectors); Assert.Empty(action.Selectors[0].ActionConstraints.OfType()); Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] public void CreateActionModel_ConventionallyRoutedAction_WithHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo(); var actionName = nameof(ConventionallyRoutedController.Update); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Contains("PUT", methodConstraint.HttpMethods); Assert.Contains("PATCH", methodConstraint.HttpMethods); Assert.Equal(actionName, action.ActionName); Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] public void CreateActionModel_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo(); var actionName = nameof(ConventionallyRoutedController.Delete); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Contains("DELETE", methodConstraint.HttpMethods); Assert.Contains("HEAD", methodConstraint.HttpMethods); Assert.Equal(actionName, action.ActionName); Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.Single(action.Attributes.OfType()); Assert.Single(action.Attributes.OfType()); } [Fact] public void CreateActionModel_ConventionallyRoutedAction_WithMultipleHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo(); var actionName = nameof(ConventionallyRoutedController.Details); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Contains("GET", methodConstraint.HttpMethods); Assert.Contains("POST", methodConstraint.HttpMethods); Assert.Contains("HEAD", methodConstraint.HttpMethods); Assert.Equal(actionName, action.ActionName); Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] public void CreateActionModel_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo(); var actionName = nameof(ConventionallyRoutedController.List); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Contains("GET", methodConstraint.HttpMethods); Assert.Contains("PUT", methodConstraint.HttpMethods); Assert.Contains("POST", methodConstraint.HttpMethods); Assert.Equal(actionName, action.ActionName); Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] public void CreateActionModel_AttributeRouteOnAction() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo(); var actionName = nameof(NoRouteAttributeOnControllerController.Edit); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Equal(actionName, action.ActionName); var httpMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("HEAD", httpMethod); var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); Assert.Equal("Change", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] public void CreateActionModel_AttributeRouteOnAction_RouteAttribute() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo(); var actionName = nameof(NoRouteAttributeOnControllerController.Update); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); Assert.Empty(action.Selectors[0].ActionConstraints); Assert.Equal(actionName, action.ActionName); var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); Assert.Equal("Update", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] public void CreateActionModel_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo(); var actionName = nameof(NoRouteAttributeOnControllerController.List); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Equal(actionName, action.ActionName); Assert.Equal( new[] { "GET", "HEAD" }, methodConstraint.HttpMethods.OrderBy(m => m, StringComparer.Ordinal)); var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); Assert.Equal("ListAll", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] public void CreateActionModel_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo(); var actionName = nameof(NoRouteAttributeOnControllerController.Index); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Equal(actionName, action.ActionName); Assert.NotNull(action.Attributes); Assert.Equal(2, action.Attributes.Count); Assert.Single(action.Attributes.OfType()); Assert.Single(action.Attributes.OfType()); Assert.Equal(2, action.Selectors.Count); foreach (var actionSelectorModel in action.Selectors) { Assert.NotNull(actionSelectorModel.AttributeRouteModel); } var selectorModel = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "List"); var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); var listMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("POST", listMethod); var all = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "All"); methodConstraint = Assert.Single(all.ActionConstraints.OfType()); var allMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("GET", allMethod); } [Fact] public void CreateActionModel_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo(); var actionName = nameof(NoRouteAttributeOnControllerController.Remove); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Equal(actionName, action.ActionName); Assert.Empty(action.Attributes); Assert.Single(action.Selectors); Assert.Empty(action.Selectors[0].ActionConstraints); Assert.Null(action.Selectors[0].AttributeRouteModel); } [Theory] [InlineData(typeof(SingleRouteAttributeController))] [InlineData(typeof(MultipleRouteAttributeController))] public void CreateActionModel_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller) { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = controller.GetTypeInfo(); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Delete")); // Assert Assert.NotNull(action); Assert.Equal("Delete", action.ActionName); Assert.Single(action.Selectors); Assert.Empty(action.Selectors[0].ActionConstraints); Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.Empty(action.Attributes); } [Theory] [InlineData(typeof(SingleRouteAttributeController))] [InlineData(typeof(MultipleRouteAttributeController))] public void CreateActionModel_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller) { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = controller.GetTypeInfo(); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Index")); // Assert Assert.NotNull(action.Attributes); Assert.Equal(2, action.Attributes.Count); Assert.Equal(2, action.Selectors.Count); Assert.Equal("Index", action.ActionName); foreach (var selectorModel in action.Selectors) { var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); var httpMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("GET", httpMethod); Assert.NotNull(selectorModel.AttributeRouteModel.Template); } Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("List")); Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("All")); } [Fact] public void CreateActionModel_MixedHttpVerbsAndRoutes_EmptyVerbWithRoute() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo(); var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.VerbAndRoute); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Single(action.Selectors); var methodConstraint = Assert.Single( action.Selectors[0].ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); Assert.Equal("Products", attributeRoute.Template); } [Fact] public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyVerbsWithMultipleRoutes() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo(); var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsAndRoutes); // Act var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.Equal(2, actions.Selectors.Count); // OrderBy is used because the order of the results may very depending on the platform / client. var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "Products"); var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "v2/Products"); methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); } [Fact] public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbsWithMultipleRoutes() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo(); var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsWithAnyWithoutTemplateAndRoutes); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.Equal(3, action.Selectors.Count); var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products"); var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "v2/Products"); methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products/Buy"); methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "POST" }, methodConstraint.HttpMethods); } [Fact] public void CreateActionModel_MixedHttpVerbsAndRoutes_WithRouteOnController() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(RouteAttributeOnController).GetTypeInfo(); var actionName = nameof(RouteAttributeOnController.Get); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.Equal(2, action.Selectors.Count); var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel == null); var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == "id/{id?}"); methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); } [Fact] public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbs() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(MixedHttpVerbsAndRouteAttributeController).GetTypeInfo(); var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.Invalid); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.NotNull(action); Assert.Equal(2, action.Selectors.Count); var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == "Products"); var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "POST" }, methodConstraint.HttpMethods); selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == null); methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); } [Fact] public void CreateActionModel_InheritedAttributeRoutes() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedClassInheritsAttributeRoutesController).GetTypeInfo(); var actionName = nameof(DerivedClassInheritsAttributeRoutesController.Edit); // Act var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.Equal(2, actions.Attributes.Count); Assert.Equal(2, actions.Selectors.Count); var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "A"); Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes); selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "B"); Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes); } [Fact] public void CreateActionModel_InheritedAttributeRoutesOverridden() { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = typeof(DerivedClassOverridesAttributeRoutesController).GetTypeInfo(); var actionName = nameof(DerivedClassOverridesAttributeRoutesController.Edit); // Act var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert Assert.Equal(4, action.Attributes.Count); Assert.Equal(2, action.Selectors.Count); var selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "C"); Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes); selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "D"); Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes); } private IList GetAttributeRoutes(IList selectors) { return selectors .Where(sm => sm.AttributeRouteModel != null) .Select(sm => sm.AttributeRouteModel) .ToList(); } private class BaseClassWithAttributeRoutesController { [Route("A")] [Route("B")] public virtual void Edit() { } } private class DerivedClassInheritsAttributeRoutesController : BaseClassWithAttributeRoutesController { public override void Edit() { } } private class DerivedClassOverridesAttributeRoutesController : BaseClassWithAttributeRoutesController { [Route("C")] [Route("D")] public override void Edit() { } } private class Controller : IDisposable { public void Dispose() { throw new NotImplementedException(); } [NonAction] public virtual IActionResult Redirect(string url) { return null; } } private class BaseController : Controller { public void GetFromBase() // Valid action method. { } [NonAction] public virtual void OverridenNonActionMethod() { } [NonAction] public virtual void NewMethod() { } public override IActionResult Redirect(string url) { return base.Redirect(url + "#RedirectOverride"); } } private class DerivedController : BaseController { public void GetFromDerived() // Valid action method. { } [HttpGet] public override void OverridenNonActionMethod() { } public new void NewMethod() // Valid action method. { } public void GenericMethod() { } private void PrivateMethod() { } public static void StaticMethod() { } protected static void ProtectedStaticMethod() { } private static void PrivateStaticMethod() { } public string Dispose(string s) { return s; } public new void Dispose() { } } private class IDisposablePocoController : IDisposable { public string Index() { return "Hello world"; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } public string Dispose(string s) { return s; } } private class BaseClass : IDisposable { public virtual void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } } private class DerivedOverriddenDisposeController : BaseClass { public override void Dispose() { base.Dispose(); } } private class DerivedNewDisposeController : BaseClass { public new void Dispose() { base.Dispose(); } } private class SimplePocoController { public string Index() { return "Hello world"; } public void Dispose() { } public void Dispose(string s) { } } private class OperatorOverloadingController : Controller { public static OperatorOverloadingController operator +( OperatorOverloadingController c1, OperatorOverloadingController c2) { return new OperatorOverloadingController(); } } private class NoRouteAttributeOnControllerController : Controller { [HttpGet("All")] [HttpPost("List")] public void Index() { } [HttpHead("Change")] public void Edit() { } public void Remove() { } [Route("Update")] public void Update() { } [AcceptVerbs("GET", "HEAD", Route = "ListAll")] public void List() { } } [Route("Products")] private class SingleRouteAttributeController : Controller { [HttpGet("All")] [HttpGet("List")] public void Index() { } public void Delete() { } } [Route("Products")] [Route("Items")] private class MultipleRouteAttributeController : Controller { [HttpGet("All")] [HttpGet("List")] public void Index() { } public void Delete() { } } private class MixedHttpVerbsAndRouteAttributeController : Controller { // Should produce a single action constrained to GET [HttpGet] [Route("Products")] public void VerbAndRoute() { } // Should produce two actions constrained to GET,POST [HttpGet] [HttpPost] [Route("Products")] [Route("v2/Products")] public void MultipleVerbsAndRoutes() { } // Produces: // // Products - GET // v2/Products - GET // Products/Buy - POST [HttpGet] [Route("Products")] [Route("v2/Products")] [HttpPost("Products/Buy")] public void MultipleVerbsWithAnyWithoutTemplateAndRoutes() { } // Produces: // // (no route) - GET // Products - POST // // This is invalid, and will throw during the ADP construction phase. [HttpGet] [HttpPost("Products")] public void Invalid() { } } [Route("api/[controller]")] private class RouteAttributeOnController : Controller { [HttpGet] [HttpGet("id/{id?}")] public object Get(short? id) { return null; } [HttpDelete("{id}")] public object Delete(int id) { return null; } } // Here the constraints on the methods are acting as an IActionHttpMethodProvider and // not as an IRouteTemplateProvider given that there is no RouteAttribute // on the controller and the template for all the constraints on a method is null. private class ConventionallyRoutedController : Controller { public void Edit() { } [CustomHttpMethods("PUT", "PATCH")] public void Update() { } [HttpHead] [HttpDelete] public void Delete() { } [HttpPost] [HttpGet] [HttpHead] public void Details() { } [HttpGet] [HttpPut] [AcceptVerbs("GET", "POST")] public void List() { } } private class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider { private readonly string[] _methods; public CustomHttpMethodsAttribute(params string[] methods) { _methods = methods; } public IEnumerable HttpMethods { get { return _methods; } } } [Route("A")] [Route("B")] private class BaseClassWithRoutesController { } private class DerivedClassInheritingRoutesController : BaseClassWithRoutesController { } [Route("C")] [Route("D")] private class DerivedClassHidingRoutesController : BaseClassWithRoutesController { } private class StoreController : Controller, IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { throw new NotImplementedException(); } public void OnActionExecuting(ActionExecutingContext context) { throw new NotImplementedException(); } } private class MyFilterAttribute : Attribute, IFilterMetadata { } [MyFilter] public class NoFiltersController { } public class ModelBinderController { [FromQuery] public string Bound { get; set; } public string Unbound { get; set; } } public class SomeFiltersController : IAsyncActionFilter, IResultFilter { public Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { return null; } public void OnResultExecuted(ResultExecutedContext context) { } public void OnResultExecuting(ResultExecutingContext context) { } } private class UnsupportedFiltersController : IExceptionFilter, IAuthorizationFilter, IAsyncResourceFilter { public void OnAuthorization(AuthorizationFilterContext context) { throw new NotImplementedException(); } public void OnException(ExceptionContext context) { throw new NotImplementedException(); } public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { throw new NotImplementedException(); } } private class TestApplicationModelProvider : DefaultApplicationModelProvider { public TestApplicationModelProvider() : this(new TestOptionsManager()) { } public TestApplicationModelProvider( IOptions options) : base(options) { Options = options.Value; } public MvcOptions Options { get; } public new ControllerModel CreateControllerModel(TypeInfo typeInfo) { return base.CreateControllerModel(typeInfo); } public new ActionModel CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo) { return base.CreateActionModel(typeInfo, methodInfo); } public new PropertyModel CreatePropertyModel(PropertyInfo propertyInfo) { return base.CreatePropertyModel(propertyInfo); } public new bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) { return base.IsAction(typeInfo, methodInfo); } public new ParameterModel CreateParameterModel(ParameterInfo parameterInfo) { return base.CreateParameterModel(parameterInfo); } } } }