diff --git a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs index 68810405df..63a6947aa2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorBuilder.cs @@ -544,6 +544,13 @@ namespace Microsoft.AspNet.Mvc actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Template, actionDescriptor.RouteValueDefaults); + + if (actionDescriptor.AttributeRouteInfo.Name != null) + { + actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens( + actionDescriptor.AttributeRouteInfo.Name, + actionDescriptor.RouteValueDefaults); + } } catch (InvalidOperationException ex) { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs index 82086fbae0..452d53c451 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionDescriptorProviderTests.cs @@ -864,6 +864,90 @@ namespace Microsoft.AspNet.Mvc.Test } } + [Fact] + public void AttributeRouting_RouteNameTokenReplace_AllowsMultipleActions_WithSameRouteNameTemplate() + { + // Arrange + var provider = GetProvider(typeof(ActionRouteNameTemplatesController).GetTypeInfo()); + var editActionName = nameof(ActionRouteNameTemplatesController.Edit); + var getActionName = nameof(ActionRouteNameTemplatesController.Get); + + // Act + var actions = provider.GetDescriptors(); + + // Assert + var getActions = actions.Where(a => a.Name.Equals(getActionName)); + Assert.Equal(2, getActions.Count()); + + foreach (var getAction in getActions) + { + Assert.NotNull(getAction.AttributeRouteInfo); + Assert.Equal("Products/Get", getAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase); + Assert.Equal("Products_Get", getAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase); + } + + var editAction = Assert.Single(actions, a => a.Name.Equals(editActionName)); + Assert.NotNull(editAction.AttributeRouteInfo); + Assert.Equal("Products/Edit", editAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase); + Assert.Equal("Products_Edit", editAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void AttributeRouting_RouteNameTokenReplace_AreaControllerActionTokensInRoute() + { + // Arrange + var provider = GetProvider(typeof(ControllerActionRouteNameTemplatesController).GetTypeInfo()); + var editActionName = nameof(ControllerActionRouteNameTemplatesController.Edit); + var getActionName = nameof(ControllerActionRouteNameTemplatesController.Get); + + // Act + var actions = provider.GetDescriptors(); + + // Assert + var getActions = actions.Where(a => a.Name.Equals(getActionName)); + Assert.Equal(2, getActions.Count()); + + foreach (var getAction in getActions) + { + Assert.NotNull(getAction.AttributeRouteInfo); + Assert.Equal( + "ControllerActionRouteNameTemplates/Get", + getAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase); + Assert.Equal( + "Products_ControllerActionRouteNameTemplates_Get", + getAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase); + } + + var editAction = Assert.Single(actions, a => a.Name.Equals(editActionName)); + Assert.NotNull(editAction.AttributeRouteInfo); + Assert.Equal( + "ControllerActionRouteNameTemplates/Edit", + editAction.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase); + Assert.Equal( + "Products_ControllerActionRouteNameTemplates_Edit", + editAction.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void AttributeRouting_RouteNameTokenReplace_InvalidToken() + { + // Arrange + var provider = GetProvider(typeof(RouteNameIncorrectTokenController).GetTypeInfo()); + + var expectedMessage = + "The following errors occurred with attribute routing information:" + Environment.NewLine + + Environment.NewLine + + "Error 1:" + Environment.NewLine + + "For action: 'Microsoft.AspNet.Mvc.Test.ControllerActionDescriptorProviderTests+" + + "RouteNameIncorrectTokenController.Get'" + Environment.NewLine + + "Error: While processing template 'Products_[unknown]', a replacement value for the token 'unknown' " + + "could not be found. Available tokens: 'action, controller'."; + + // Act & Assert + var ex = Assert.Throws(() => { provider.GetDescriptors(); }); + Assert.Equal(expectedMessage, ex.Message); + } + [Fact] public void AttributeRouting_RouteGroupConstraint_IsAddedOnceForNonAttributeRoutes() { @@ -1630,6 +1714,37 @@ namespace Microsoft.AspNet.Mvc.Test public void PatchItems() { } } + [Route("Products/[action]", Name = "Products_[action]")] + private class ActionRouteNameTemplatesController + { + [HttpGet] + public void Get() { } + + [HttpPost] + public void Get(int id) { } + + public void Edit() { } + } + + [Area("Products")] + [Route("[controller]/[action]", Name = "[area]_[controller]_[action]")] + private class ControllerActionRouteNameTemplatesController + { + [HttpGet] + public void Get() { } + + [HttpPost] + public void Get(int id) { } + + public void Edit() { } + } + + [Route("Products/[action]", Name = "Products_[unknown]")] + private class RouteNameIncorrectTokenController + { + public void Get() { } + } + private class DifferentCasingsAttributeRouteNamesController { [HttpGet("{id}", Name = "Products")] diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs index d1681a3f57..28de2f3e5a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs @@ -1373,6 +1373,33 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Theory] + [InlineData("/Order/Add/1", "GET", "Add")] + [InlineData("/Order/Add", "POST", "Add")] + [InlineData("/Order/Edit/1", "PUT", "Edit")] + [InlineData("/Order/GetOrder", "GET", "GetOrder")] + public async Task AttributeRouting_RouteNameTokenReplace_Reachable(string path, string verb, string actionName) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(new HttpMethod(verb), "http://localhost" + path); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains(path, result.ExpectedUrls); + Assert.Equal("Order", result.Controller); + Assert.Equal(actionName, result.Action); + } + private static LinkBuilder LinkFrom(string url) { return new LinkBuilder(url); diff --git a/test/WebSites/RoutingWebSite/Areas/Order/OrderController.cs b/test/WebSites/RoutingWebSite/Areas/Order/OrderController.cs new file mode 100644 index 0000000000..adcef8c5f1 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Areas/Order/OrderController.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RoutingWebSite.Areas.Order +{ + [Area("Order")] + [Route("Order/[action]", Name = "[area]_[action]")] + public class OrderController : Controller + { + private readonly TestResponseGenerator _generator; + + public OrderController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet] + public IActionResult GetOrder() + { + return _generator.Generate("/Order/GetOrder"); + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/OrderController.cs b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs new file mode 100644 index 0000000000..aae75a46d0 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RoutingWebSite.Controllers +{ + [Route("Order/[action]/{orderId?}", Name = "Order_[action]")] + public class OrderController : Controller + { + private readonly TestResponseGenerator _generator; + + public OrderController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet] + public IActionResult Add(int orderId) + { + return _generator.Generate("/Order/Add/1"); + } + + [HttpPost] + public IActionResult Add() + { + return _generator.Generate("/Order/Add"); + } + + [HttpPut] + public IActionResult Edit(int orderId) + { + return _generator.Generate("/Order/Edit/1"); + } + } +}