[Fixes #900] Use [Route(...)] on action methods to specify an attribute route that allows all verbs.

1. Changed attribute usage on RouteAttribute.
2. Added a test on action discovery to ensure that actions with [Route] get discovered as
   attribute routed actions.
3. Added a test on reflected action descriptor provider to ensure that an action with [Route] on
   the controller and [Route] on the action results in an action that allows any Http method.
This commit is contained in:
jacalvar 2014-09-09 16:30:28 -07:00
parent 44ccea6871
commit cde1a95d49
5 changed files with 126 additions and 3 deletions

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc
/// <summary>
/// Specifies an attribute route on a controller.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteAttribute : Attribute, IRouteTemplateProvider
{
private int? _order;

View File

@ -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

View File

@ -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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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);

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}