diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
index c7d96f1074..9fbdbc05fd 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
@@ -70,5 +70,8 @@ namespace Microsoft.AspNet.Mvc
return _order;
}
}
+
+ ///
+ public string Name { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 90327b5bbd..e444499ce1 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1275,7 +1275,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or before calling '{2}' in the application startup code.
+ /// Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.
///
internal static string UnableToFindServices
{
@@ -1283,13 +1283,77 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or before calling '{2}' in the application startup code.
+ /// Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.
///
internal static string FormatUnableToFindServices(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
}
+ ///
+ /// Two or more routes named '{0}' have different templates.
+ ///
+ internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName
+ {
+ get { return GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); }
+ }
+
+ ///
+ /// Two or more routes named '{0}' have different templates.
+ ///
+ internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0);
+ }
+
+ ///
+ /// Action: '{0}' - Template: '{1}'
+ ///
+ internal static string AttributeRoute_DuplicateNames_Item
+ {
+ get { return GetString("AttributeRoute_DuplicateNames_Item"); }
+ }
+
+ ///
+ /// Action: '{0}' - Template: '{1}'
+ ///
+ internal static string FormatAttributeRoute_DuplicateNames_Item(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DuplicateNames_Item"), p0, p1);
+ }
+
+ ///
+ /// Attribute routes with the same name '{0}' must have the same template:{1}{2}
+ ///
+ internal static string AttributeRoute_DuplicateNames
+ {
+ get { return GetString("AttributeRoute_DuplicateNames"); }
+ }
+
+ ///
+ /// Attribute routes with the same name '{0}' must have the same template:{1}{2}
+ ///
+ internal static string FormatAttributeRoute_DuplicateNames(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DuplicateNames"), p0, p1, p2);
+ }
+
+ ///
+ /// Error {0}:{1}{2}
+ ///
+ internal static string AttributeRoute_AggregateErrorMessage_ErrorNumber
+ {
+ get { return GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"); }
+ }
+
+ ///
+ /// Error {0}:{1}{2}
+ ///
+ internal static string FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"), p0, p1, p2);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
index 526d2ad2a2..0c4d37b32f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
@@ -161,7 +161,8 @@ namespace Microsoft.AspNet.Mvc
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
{
Template = combinedRoute.Template,
- Order = combinedRoute.Order ?? DefaultAttributeRouteOrder
+ Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
+ Name = combinedRoute.Name,
};
var actionDescriptor = new ReflectedActionDescriptor()
@@ -291,6 +292,9 @@ namespace Microsoft.AspNet.Mvc
}
}
+ var actionsByRouteName = new Dictionary>(
+ StringComparer.OrdinalIgnoreCase);
+
foreach (var actionDescriptor in actions)
{
if (actionDescriptor.AttributeRouteInfo == null ||
@@ -317,6 +321,25 @@ namespace Microsoft.AspNet.Mvc
}
else
{
+ var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
+ if (attributeRouteInfo.Name != null)
+ {
+ // Build a map of attribute route name to action descriptors to ensure that all
+ // attribute routes with a given name have the same template.
+ IList namedActionGroup;
+
+ if (actionsByRouteName.TryGetValue(attributeRouteInfo.Name, out namedActionGroup))
+ {
+ namedActionGroup.Add(actionDescriptor);
+ }
+ else
+ {
+ namedActionGroup = new List();
+ namedActionGroup.Add(actionDescriptor);
+ actionsByRouteName.Add(attributeRouteInfo.Name, namedActionGroup);
+ }
+ }
+
// We still want to add a 'null' for any constraint with DenyKey so that link generation
// works properly.
//
@@ -332,17 +355,86 @@ namespace Microsoft.AspNet.Mvc
}
}
+ var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
+ if (namedRoutedErrors.Any())
+ {
+ namedRoutedErrors = AddErrorNumbers(namedRoutedErrors);
+
+ var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
+ Environment.NewLine,
+ string.Join(Environment.NewLine + Environment.NewLine, namedRoutedErrors));
+
+ throw new InvalidOperationException(message);
+ }
+
if (routeTemplateErrors.Any())
{
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
+
throw new InvalidOperationException(message);
}
return actions;
}
+ private static IList AddErrorNumbers(IList namedRoutedErrors)
+ {
+ return namedRoutedErrors
+ .Select((nre, i) =>
+ Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
+ i + 1,
+ Environment.NewLine,
+ nre))
+ .ToList();
+ }
+
+ private static IList ValidateNamedAttributeRoutedActions(
+ IDictionary> actionsGroupedByRouteName)
+ {
+ var namedRouteErrors = new List();
+
+ foreach (var kvp in actionsGroupedByRouteName)
+ {
+ // We are looking for attribute routed actions that have the same name but
+ // different route templates. We pick the first template of the group and
+ // we compare it against the rest of the templates that have that same name
+ // associated.
+ // The moment we find one that is different we report the whole group to the
+ // user in the error message so that he can see the different actions and the
+ // different templates for a given named attribute route.
+ var firstActionDescriptor = kvp.Value[0];
+ var firstTemplate = firstActionDescriptor.AttributeRouteInfo.Template;
+
+ for (var i = 1; i < kvp.Value.Count; i++)
+ {
+ var otherActionDescriptor = kvp.Value[i];
+ var otherActionTemplate = otherActionDescriptor.AttributeRouteInfo.Template;
+
+ if (!firstTemplate.Equals(otherActionTemplate, StringComparison.OrdinalIgnoreCase))
+ {
+ var descriptions = kvp.Value.Select(ad =>
+ Resources.FormatAttributeRoute_DuplicateNames_Item(
+ ad.DisplayName,
+ ad.AttributeRouteInfo.Template));
+
+ var errorDescription = string.Join(Environment.NewLine, descriptions);
+ var message = Resources.FormatAttributeRoute_DuplicateNames(
+ kvp.Key,
+ Environment.NewLine,
+ errorDescription);
+
+ namedRouteErrors.Add(message);
+ break;
+ }
+ }
+ }
+
+ return namedRouteErrors;
+ }
+
private static string GetRouteGroupValue(int order, string template)
{
var group = string.Format("{0}-{1}", order, template);
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
index a58f46e490..a0dc996058 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
@@ -21,12 +21,15 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
Template = templateProvider.Template;
Order = templateProvider.Order;
+ Name = templateProvider.Name;
}
public string Template { get; set; }
public int? Order { get; set; }
+ public string Name { get; set; }
+
///
/// Combines two instances and returns
/// a new instance with the result.
@@ -60,10 +63,25 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
return new ReflectedAttributeRouteModel()
{
Template = combinedTemplate,
- Order = right.Order ?? left.Order
+ Order = right.Order ?? left.Order,
+ Name = ChooseName(left, right),
};
}
+ private static string ChooseName(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right)
+ {
+ if (right.Name == null && string.IsNullOrEmpty(right.Template))
+ {
+ return left.Name;
+ }
+ else
+ {
+ return right.Name;
+ }
+ }
+
internal static string CombineTemplates(string left, string right)
{
var result = CombineCore(left, right);
@@ -252,7 +270,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
// This is an unclosed replacement token
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
- template,
+ template,
Resources.AttributeRoute_TokenReplacement_UnclosedToken);
throw new InvalidOperationException(message);
}
@@ -272,7 +290,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
// Unescaped left-bracket is not allowed inside a token.
var message = Resources.FormatAttributeRoute_TokenReplacement_InvalidSyntax(
- template,
+ template,
Resources.AttributeRoute_TokenReplacement_UnescapedBraceInToken);
throw new InvalidOperationException(message);
}
@@ -303,7 +321,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
}
builder.Append(value);
-
+
if (c == '[')
{
state = TemplateParserState.SeenLeft;
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index 11507e0ba0..d08914297f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -360,4 +360,19 @@
Unable to find the required services. Please add all the required services by calling '{0}' inside the call to '{1}' or '{2}' in the application startup code.
+
+ Two or more routes named '{0}' have different templates.
+
+
+ Action: '{0}' - Template: '{1}'
+ Formats an action descriptor display name and it's associated template.
+
+
+ Attribute routes with the same name '{0}' must have the same template:{1}{2}
+ {0} is the name of the attribute route, {1} is the newline, {2} is the list of errors formatted using ActionDescriptor_WithNamedAttributeRouteAndDifferentTemplate
+
+
+ Error {0}:{1}{2}
+ {0} is the error number, {1} is Environment.NewLine {2} is the error message
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
index 138215762f..217e2afdf6 100644
--- a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
@@ -46,5 +46,8 @@ namespace Microsoft.AspNet.Mvc
return _order;
}
}
+
+ ///
+ public string Name { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
index 7e53483a04..6c31c13858 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing;
@@ -20,6 +21,8 @@ namespace Microsoft.AspNet.Mvc.Routing
{
private readonly IRouter _next;
private readonly TemplateRoute[] _matchingRoutes;
+ private readonly IDictionary _namedEntries;
+
private ILogger _logger;
private ILogger _constraintLogger;
private readonly LinkGenerationDecisionTree _linkGenerationTree;
@@ -49,6 +52,36 @@ namespace Microsoft.AspNet.Mvc.Routing
.Select(e => e.Route)
.ToArray();
+ var namedEntries = new Dictionary(
+ StringComparer.OrdinalIgnoreCase);
+
+ foreach (var entry in linkGenerationEntries)
+ {
+ // Skip unnamed entries
+ if (entry.Name == null)
+ {
+ continue;
+ }
+
+ // We only need to keep one AttributeRouteLinkGenerationEntry per route template
+ // so in case two entries have the same name and the same template we only keep
+ // the first entry.
+ AttributeRouteLinkGenerationEntry namedEntry = null;
+ if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
+ !namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(
+ Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
+ "linkGenerationEntries");
+ }
+ else if (namedEntry == null)
+ {
+ namedEntries.Add(entry.Name, entry);
+ }
+ }
+
+ _namedEntries = namedEntries;
+
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
@@ -61,12 +94,12 @@ namespace Microsoft.AspNet.Mvc.Routing
{
using (_logger.BeginScope("AttributeRoute.RouteAsync"))
{
- foreach (var route in _matchingRoutes)
- {
- await route.RouteAsync(context);
-
- if (context.IsHandled)
+ foreach (var route in _matchingRoutes)
{
+ await route.RouteAsync(context);
+
+ if (context.IsHandled)
+ {
break;
}
}
@@ -85,6 +118,13 @@ namespace Microsoft.AspNet.Mvc.Routing
///
public string GetVirtualPath([NotNull] VirtualPathContext context)
{
+ // If it's a named route we will try to generate a link directly and
+ // if we can't, we will not try to generate it using an unnamed route.
+ if (context.RouteName != null)
+ {
+ return GetVirtualPathForNamedRoute(context);
+ }
+
// The decision tree will give us back all entries that match the provided route data in the correct
// order. We just need to iterate them and use the first one that can generate a link.
var matches = _linkGenerationTree.GetMatches(context);
@@ -102,6 +142,21 @@ namespace Microsoft.AspNet.Mvc.Routing
return null;
}
+ private string GetVirtualPathForNamedRoute(VirtualPathContext context)
+ {
+ AttributeRouteLinkGenerationEntry entry;
+ if (_namedEntries.TryGetValue(context.RouteName, out entry))
+ {
+ var path = GenerateLink(context, entry);
+ if (path != null)
+ {
+ context.IsBound = true;
+ return path;
+ }
+ }
+ return null;
+ }
+
private string GenerateLink(VirtualPathContext context, AttributeRouteLinkGenerationEntry entry)
{
// In attribute the context includes the values that are used to select this entry - typically
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
index 463de8c510..22544bcb86 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
@@ -14,10 +14,17 @@ namespace Microsoft.AspNet.Mvc.Routing
public string Template { get; set; }
///
- /// Gets the order of the route associated with this . This property determines
+ /// Gets the order of the route associated with a given action. This property determines
/// the order in which routes get executed. Routes with a lower order value are tried first. In case a route
/// doesn't specify a value, it gets a default order of 0.
///
public int Order { get; set; }
+
+ ///
+ /// Gets the name of the route associated with a given action. This property can be used
+ /// to generate a link by referring to the route by name instead of attempting to match a
+ /// route by provided route data.
+ ///
+ public string Name { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
index f088991513..8510f8e310 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
@@ -38,6 +38,11 @@ namespace Microsoft.AspNet.Mvc.Routing
///
public decimal Precedence { get; set; }
+ ///
+ /// The name of the route.
+ ///
+ public string Name { get; set; }
+
///
/// The route group.
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
index a9922c846f..f85f46e05a 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
@@ -46,7 +46,8 @@ namespace Microsoft.AspNet.Mvc.Routing
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
RouteGroup = routeInfo.RouteGroup,
Template = routeInfo.ParsedTemplate,
- TemplateText = routeInfo.RouteTemplate
+ TemplateText = routeInfo.RouteTemplate,
+ Name = routeInfo.Name,
});
}
@@ -215,6 +216,8 @@ namespace Microsoft.AspNet.Mvc.Routing
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
+ routeInfo.Name = action.AttributeRouteInfo.Name;
+
routeInfo.Constraints = routeInfo.ParsedTemplate.Parameters
.Where(p => p.InlineConstraint != null)
.ToDictionary(p => p.Name, p => p.InlineConstraint, StringComparer.OrdinalIgnoreCase);
@@ -245,6 +248,8 @@ namespace Microsoft.AspNet.Mvc.Routing
public string RouteGroup { get; set; }
public string RouteTemplate { get; set; }
+
+ public string Name { get; set; }
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
index 4cbd7ff3d1..bb4415b8bf 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
@@ -20,5 +20,11 @@ namespace Microsoft.AspNet.Mvc.Routing
/// route.
///
int? Order { get; }
+
+ ///
+ /// Gets the route name. The route name can be used to generate a link using a specific route, instead
+ /// of relying on selection of a route based on the given set of route values.
+ ///
+ string Name { get; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs
index b1aa28a735..a8eda4a1ee 100644
--- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs
@@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
}
///
- /// Argument must be an instance of type '{0}'.
+ /// Argument must be an instance of '{0}'.
///
internal static string ArgumentMustBeOfType
{
@@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
}
///
- /// Argument must be an instance of type '{0}'.
+ /// Argument must be an instance of '{0}'.
///
internal static string FormatArgumentMustBeOfType(object p0)
{
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs
index e4bc637118..e9ecffbbdb 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs
@@ -265,6 +265,76 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(expectedMessage, ex.Message);
}
+ [Fact]
+ public void AttributeRouting_Name_ThrowsIfMultipleActions_WithDifferentTemplatesHaveTheSameName()
+ {
+ // Arrange
+ var provider = GetProvider(typeof(SameNameDifferentTemplatesController).GetTypeInfo());
+
+ var expectedMessage =
+ "The following errors occurred with attribute routing information:"
+ + Environment.NewLine + Environment.NewLine +
+ "Error 1:" + Environment.NewLine +
+ "Attribute routes with the same name 'Products' must have the same template:"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.Get' - Template: 'Products'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.Get' - Template: 'Products/{id}'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.Put' - Template: 'Products/{id}'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.Post' - Template: 'Products'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.Delete' - Template: 'Products/{id}'"
+ + Environment.NewLine + Environment.NewLine +
+ "Error 2:" + Environment.NewLine +
+ "Attribute routes with the same name 'Items' must have the same template:"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.GetItems' - Template: 'Items/{id}'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.PostItems' - Template: 'Items'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.PutItems' - Template: 'Items/{id}'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.DeleteItems' - Template: 'Items/{id}'"
+ + Environment.NewLine +
+ "Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
+ "SameNameDifferentTemplatesController.PatchItems' - Template: 'Items'";
+
+ // Act
+ var ex = Assert.Throws(() => { provider.GetDescriptors(); });
+
+ // Assert
+ Assert.Equal(expectedMessage, ex.Message);
+ }
+
+ [Fact]
+ public void AttributeRouting_Name_AllowsMultipleAttributeRoutesInDifferentActions_WithTheSameNameAndTemplate()
+ {
+ // Arrange
+ var provider = GetProvider(typeof(DifferentCasingsAttributeRouteNamesController).GetTypeInfo());
+
+ // Act
+ var descriptors = provider.GetDescriptors();
+
+ // Assert
+ foreach (var descriptor in descriptors)
+ {
+ Assert.NotNull(descriptor.AttributeRouteInfo);
+ Assert.Equal("{id}", descriptor.AttributeRouteInfo.Template, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("Products", descriptor.AttributeRouteInfo.Name, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
[Fact]
public void AttributeRouting_RouteGroupConstraint_IsAddedOnceForNonAttributeRoutes()
{
@@ -564,6 +634,55 @@ namespace Microsoft.AspNet.Mvc.Test
public void AnotherNonAttributedAction() { }
}
+ [Route("Products", Name = "Products")]
+ public class SameNameDifferentTemplatesController
+ {
+ [HttpGet]
+ public void Get() { }
+
+ [HttpGet("{id}", Name = "Products")]
+ public void Get(int id) { }
+
+ [HttpPut("{id}", Name = "Products")]
+ public void Put(int id) { }
+
+ [HttpPost]
+ public void Post() { }
+
+ [HttpDelete("{id}", Name = "Products")]
+ public void Delete(int id) { }
+
+ [HttpGet("/Items/{id}", Name = "Items")]
+ public void GetItems(int id) { }
+
+ [HttpPost("/Items", Name = "Items")]
+ public void PostItems() { }
+
+ [HttpPut("/Items/{id}", Name = "Items")]
+ public void PutItems(int id) { }
+
+ [HttpDelete("/Items/{id}", Name = "Items")]
+ public void DeleteItems(int id) { }
+
+ [HttpPatch("/Items", Name = "Items")]
+ public void PatchItems() { }
+ }
+
+ public class DifferentCasingsAttributeRouteNamesController
+ {
+ [HttpGet("{id}", Name = "Products")]
+ public void Get() { }
+
+ [HttpGet("{ID}", Name = "Products")]
+ public void Get(int id) { }
+
+ [HttpPut("{id}", Name = "PRODUCTS")]
+ public void Put(int id) { }
+
+ [HttpDelete("{ID}", Order = 1, Name = "PRODUCTS")]
+ public void Delete(int id) { }
+ }
+
[MyRouteConstraint(blockNonAttributedActions: true)]
[MySecondRouteConstraint(blockNonAttributedActions: true)]
private class ConstrainedController
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
index 304b6a9a90..2ad2d8952d 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
@@ -229,10 +229,79 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
Assert.Equal(combined.Order, right.Order);
}
+ [Theory]
+ [MemberData("CombineNamesTestData")]
+ public void Combine_Names(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right,
+ string expectedName)
+ {
+ // Arrange & Act
+ var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
+
+ // Assert
+ Assert.NotNull(combined);
+ Assert.Equal(expectedName, combined.Name);
+ }
+
+ public static IEnumerable