diff --git a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs index 217e2afdf6..b2ffed2e18 100644 --- a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc /// /// Specifies an attribute route on a controller. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RouteAttribute : Attribute, IRouteTemplateProvider { private int? _order; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs index ac54f90dc1..fb03777e6f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs @@ -566,6 +566,27 @@ namespace Microsoft.AspNet.Mvc.Test Assert.Equal(expectedMessage, ex.Message); } + [Fact] + public void AttributeRouting_RouteOnControllerAndAction_CreatesActionDescriptorWithoutHttpConstraints() + { + // Arrange + var provider = GetProvider(typeof(OnlyRouteController).GetTypeInfo()); + + // Act + var actions = provider.GetDescriptors(); + + // Assert + var action = Assert.Single(actions); + + Assert.Equal("Action", action.Name); + Assert.Equal("OnlyRoute", action.ControllerName); + + Assert.NotNull(action.AttributeRouteInfo); + Assert.Equal("Products/Index", action.AttributeRouteInfo.Template); + + Assert.Null(action.MethodConstraints); + } + [Fact] public void AttributeRouting_Name_ThrowsIfMultipleActions_WithDifferentTemplatesHaveTheSameName() { @@ -992,6 +1013,13 @@ namespace Microsoft.AspNet.Mvc.Test public void Delete(int id) { } } + [Route("Products")] + public class OnlyRouteController + { + [Route("Index")] + public void Action() { } + } + [MyRouteConstraint(blockNonAttributedActions: true)] [MySecondRouteConstraint(blockNonAttributedActions: true)] private class ConstrainedController diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs index c6e7b2f2dc..a4ee08096a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs @@ -256,6 +256,84 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("List", result.Action); } + // We are intentionally skipping GET because we have another method with [HttpGet] on the same controller + // and a test that verifies that if you define another action with a specific verb we'll route to that + // more specific action. + [Theory] + [InlineData("PUT")] + [InlineData("POST")] + [InlineData("PATCH")] + [InlineData("DELETE")] + public async Task AttributeRoutedAction_RouteAttributeOnAction_IsReachable(string method) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Store/Shop/Orders"); + + // Act + var response = await client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/Store/Shop/Orders", result.ExpectedUrls); + Assert.Equal("Store", result.Controller); + Assert.Equal("Orders", result.Action); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + public async Task AttributeRoutedAction_RouteAttributeOnActionAndController_IsReachable(string method) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/api/Employee/5/Salary"); + + // Act + var response = await client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/api/Employee/5/Salary", result.ExpectedUrls); + Assert.Equal("Employee", result.Controller); + Assert.Equal("Salary", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_RouteAttributeOnActionAndHttpGetOnDifferentAction_ReachesHttpGetAction() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Store/Shop/Orders"); + + // Act + var response = await client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/Store/Shop/Orders", result.ExpectedUrls); + Assert.Equal("Store", result.Controller); + Assert.Equal("GetOrders", result.Action); + } + // There's no [HttpGet] on the action here. [Theory] [InlineData("PUT")] @@ -276,7 +354,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject(body); - // Assert Assert.Contains("/api/Employee", result.ExpectedUrls); Assert.Equal("Employee", result.Controller); Assert.Equal("UpdateEmployee", result.Action); @@ -421,7 +498,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } [Fact] - public async Task AttributeRoutedAction__LinkGeneration_OrderOnActionOverridesOrderOnController() + public async Task AttributeRoutedAction_LinkGeneration_OrderOnActionOverridesOrderOnController() { // Arrange var server = TestServer.Create(_services, _app); diff --git a/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs b/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs index 9c9b796ccc..a712afb8e3 100644 --- a/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs @@ -55,5 +55,11 @@ namespace RoutingWebSite { return _generator.Generate("/api/Employee/" + id + "/Administrator"); } + + [Route("{id}/Salary")] + public IActionResult Salary(int id) + { + return _generator.Generate("/api/Employee/" + id + "/Salary"); + } } } \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/Controllers/StoreController.cs b/test/WebSites/RoutingWebSite/Controllers/StoreController.cs index 14150b65fa..1e12fdaee1 100644 --- a/test/WebSites/RoutingWebSite/Controllers/StoreController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/StoreController.cs @@ -27,5 +27,17 @@ namespace RoutingWebSite { return _generator.Generate("/Home/About"); } + + [Route("Store/Shop/Orders")] + public IActionResult Orders() + { + return _generator.Generate("/Store/Shop/Orders"); + } + + [HttpGet("Store/Shop/Orders")] + public IActionResult GetOrders() + { + return _generator.Generate("/Store/Shop/Orders"); + } } } \ No newline at end of file