[Fixes #734] Attribute Routing: Implement Name

1. Added support for Name in attribute routing. Name can be defined using [RouteAttribute]
and the different Http*Attributes, for example [HttpGet].

2. Names defined on actions always override names defined on the controller.

3. Actions with a non empty template don't inherit the name from the controller. The name
   is only inherited from the controller when the action template is null or empty.

4. Multiple attribute routes with different templates and the same name are not allowed.
This commit is contained in:
jacalvar 2014-08-21 12:49:38 -07:00
parent a931e21456
commit ccc20a38c1
21 changed files with 1020 additions and 24 deletions

View File

@ -70,5 +70,8 @@ namespace Microsoft.AspNet.Mvc
return _order;
}
}
/// <inheritdoc />
public string Name { get; set; }
}
}

View File

@ -1275,7 +1275,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <summary>
/// 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.
/// </summary>
internal static string UnableToFindServices
{
@ -1283,13 +1283,77 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatUnableToFindServices(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
}
/// <summary>
/// Two or more routes named '{0}' have different templates.
/// </summary>
internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName
{
get { return GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"); }
}
/// <summary>
/// Two or more routes named '{0}' have different templates.
/// </summary>
internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0);
}
/// <summary>
/// Action: '{0}' - Template: '{1}'
/// </summary>
internal static string AttributeRoute_DuplicateNames_Item
{
get { return GetString("AttributeRoute_DuplicateNames_Item"); }
}
/// <summary>
/// Action: '{0}' - Template: '{1}'
/// </summary>
internal static string FormatAttributeRoute_DuplicateNames_Item(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DuplicateNames_Item"), p0, p1);
}
/// <summary>
/// Attribute routes with the same name '{0}' must have the same template:{1}{2}
/// </summary>
internal static string AttributeRoute_DuplicateNames
{
get { return GetString("AttributeRoute_DuplicateNames"); }
}
/// <summary>
/// Attribute routes with the same name '{0}' must have the same template:{1}{2}
/// </summary>
internal static string FormatAttributeRoute_DuplicateNames(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DuplicateNames"), p0, p1, p2);
}
/// <summary>
/// Error {0}:{1}{2}
/// </summary>
internal static string AttributeRoute_AggregateErrorMessage_ErrorNumber
{
get { return GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"); }
}
/// <summary>
/// Error {0}:{1}{2}
/// </summary>
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);

View File

@ -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<string, IList<ActionDescriptor>>(
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<ActionDescriptor> namedActionGroup;
if (actionsByRouteName.TryGetValue(attributeRouteInfo.Name, out namedActionGroup))
{
namedActionGroup.Add(actionDescriptor);
}
else
{
namedActionGroup = new List<ActionDescriptor>();
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<string> AddErrorNumbers(IList<string> namedRoutedErrors)
{
return namedRoutedErrors
.Select((nre, i) =>
Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
i + 1,
Environment.NewLine,
nre))
.ToList();
}
private static IList<string> ValidateNamedAttributeRoutedActions(
IDictionary<string,
IList<ActionDescriptor>> actionsGroupedByRouteName)
{
var namedRouteErrors = new List<string>();
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);

View File

@ -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; }
/// <summary>
/// Combines two <see cref="ReflectedAttributeRouteModel"/> instances and returns
/// a new <see cref="ReflectedAttributeRouteModel"/> 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;

View File

@ -360,4 +360,19 @@
<data name="UnableToFindServices" xml:space="preserve">
<value>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.</value>
</data>
<data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
<value>Two or more routes named '{0}' have different templates.</value>
</data>
<data name="AttributeRoute_DuplicateNames_Item" xml:space="preserve">
<value>Action: '{0}' - Template: '{1}'</value>
<comment>Formats an action descriptor display name and it's associated template.</comment>
</data>
<data name="AttributeRoute_DuplicateNames" xml:space="preserve">
<value>Attribute routes with the same name '{0}' must have the same template:{1}{2}</value>
<comment>{0} is the name of the attribute route, {1} is the newline, {2} is the list of errors formatted using ActionDescriptor_WithNamedAttributeRouteAndDifferentTemplate</comment>
</data>
<data name="AttributeRoute_AggregateErrorMessage_ErrorNumber" xml:space="preserve">
<value>Error {0}:{1}{2}</value>
<comment>{0} is the error number, {1} is Environment.NewLine {2} is the error message</comment>
</data>
</root>

View File

@ -46,5 +46,8 @@ namespace Microsoft.AspNet.Mvc
return _order;
}
}
/// <inheritdoc />
public string Name { get; set; }
}
}

View File

@ -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<string, AttributeRouteLinkGenerationEntry> _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<string, AttributeRouteLinkGenerationEntry>(
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
/// <inheritdoc />
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

View File

@ -14,10 +14,17 @@ namespace Microsoft.AspNet.Mvc.Routing
public string Template { get; set; }
/// <summary>
/// Gets the order of the route associated with this <see cref="ActionDescriptor"/>. 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.
/// </summary>
public int Order { get; set; }
/// <summary>
/// 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.
/// </summary>
public string Name { get; set; }
}
}

View File

@ -38,6 +38,11 @@ namespace Microsoft.AspNet.Mvc.Routing
/// </summary>
public decimal Precedence { get; set; }
/// <summary>
/// The name of the route.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The route group.
/// </summary>

View File

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

View File

@ -20,5 +20,11 @@ namespace Microsoft.AspNet.Mvc.Routing
/// route.
/// </summary>
int? Order { get; }
/// <summary>
/// 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.
/// </summary>
string Name { get; }
}
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
}
/// <summary>
/// Argument must be an instance of type '{0}'.
/// Argument must be an instance of '{0}'.
/// </summary>
internal static string ArgumentMustBeOfType
{
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host
}
/// <summary>
/// Argument must be an instance of type '{0}'.
/// Argument must be an instance of '{0}'.
/// </summary>
internal static string FormatArgumentMustBeOfType(object p0)
{

View File

@ -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<InvalidOperationException>(() => { 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

View File

@ -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<object[]> CombineNamesTestData
{
get
{
// AttributeRoute on the controller, attribute route on the action, expected name.
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, string>();
// Combined name is null if no name is provided.
data.Add(Create(template: "/", order: null, name: null), null, null);
data.Add(Create(template: "~/", order: null, name: null), null, null);
data.Add(Create(template: "", order: null, name: null), null, null);
data.Add(Create(template: "home", order: null, name: null), null, null);
data.Add(Create(template: "/", order: 1, name: null), null, null);
data.Add(Create(template: "~/", order: 1, name: null), null, null);
data.Add(Create(template: "", order: 1, name: null), null, null);
data.Add(Create(template: "home", order: 1, name: null), null, null);
// Combined name is inherited if no right name is provided and the template is empty.
data.Add(Create(template: "/", order: null, name: "Named"), null, "Named");
data.Add(Create(template: "~/", order: null, name: "Named"), null, "Named");
data.Add(Create(template: "", order: null, name: "Named"), null, "Named");
data.Add(Create(template: "home", order: null, name: "Named"), null, "Named");
data.Add(Create(template: "home", order: null, name: "Named"), Create(null, null, null), "Named");
data.Add(Create(template: "", order: null, name: "Named"), Create("", null, null), "Named");
// Order doesn't matter for combining the name.
data.Add(Create(template: "", order: null, name: "Named"), Create("", 1, null), "Named");
data.Add(Create(template: "", order: 1, name: "Named"), Create("", 1, null), "Named");
data.Add(Create(template: "", order: 2, name: "Named"), Create("", 1, null), "Named");
data.Add(Create(template: "", order: null, name: "Named"), Create("index", 1, null), null);
data.Add(Create(template: "", order: 1, name: "Named"), Create("index", 1, null), null);
data.Add(Create(template: "", order: 2, name: "Named"), Create("index", 1, null), null);
data.Add(Create(template: "", order: null, name: "Named"), Create("", 1, "right"), "right");
data.Add(Create(template: "", order: 1, name: "Named"), Create("", 1, "right"), "right");
data.Add(Create(template: "", order: 2, name: "Named"), Create("", 1, "right"), "right");
// Combined name is not inherited if right name is provided or the template is not empty.
data.Add(Create(template: "/", order: null, name: "Named"), Create(null, null, "right"), "right");
data.Add(Create(template: "~/", order: null, name: "Named"), Create(null, null, "right"), "right");
data.Add(Create(template: "", order: null, name: "Named"), Create(null, null, "right"), "right");
data.Add(Create(template: "home", order: null, name: "Named"), Create(null, null, "right"), "right");
data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, null), null);
data.Add(Create(template: "home", order: null, name: "Named"), Create("/", null, null), null);
data.Add(Create(template: "home", order: null, name: "Named"), Create("~/", null, null), null);
data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, "right"), "right");
data.Add(Create(template: "home", order: null, name: "Named"), Create("/", null, "right"), "right");
data.Add(Create(template: "home", order: null, name: "Named"), Create("~/", null, "right"), "right");
data.Add(Create(template: "home", order: null, name: "Named"), Create("index", null, ""), "");
return data;
}
}
public static IEnumerable<object[]> CombineOrdersTestData
{
get
{
// AttributeRoute on the controller, attribute route on the action, expected order.
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, int?>();
data.Add(Create("", order: 1), Create("", order: 2), 2);
@ -261,6 +330,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
get
{
// AttributeRoute on the controller, attribute route on the action.
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
var leftModel = Create("Home", order: 3);
@ -279,7 +349,9 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
get
{
// AttributeRoute on the controller, attribute route on the action.
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
data.Add(null, null);
data.Add(null, Create(null));
data.Add(Create(null), null);
@ -293,9 +365,10 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
get
{
// AttributeRoute on the controller, attribute route on the action, expected combined attribute route.
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
data.Add(null, Create("Index"), Create("Index"));
data.Add(Create(null), Create("Index"), Create("Index"));;
data.Add(Create(null), Create("Index"), Create("Index"));
data.Add(Create("Home"), null, Create("Home"));
data.Add(Create("Home"), Create(null), Create("Home"));
data.Add(Create("Home"), Create("Index"), Create("Home/Index"));
@ -451,12 +524,13 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
}
}
private static ReflectedAttributeRouteModel Create(string template, int? order = null)
private static ReflectedAttributeRouteModel Create(string template, int? order = null, string name = null)
{
return new ReflectedAttributeRouteModel
{
Template = template,
Order = order
Order = order,
Name = name
};
}
}

View File

@ -379,6 +379,289 @@ namespace Microsoft.AspNet.Mvc.Routing
Assert.Equal(expectedGroup, selectedGroup);
}
public static IEnumerable<object[]> NamedEntriesWithDifferentTemplates
{
get
{
var data = new TheoryData<IEnumerable<AttributeRouteLinkGenerationEntry>>();
data.Add(new[]
{
CreateGenerationEntry("template", null, 0, "NamedEntry"),
CreateGenerationEntry("otherTemplate", null, 0, "NamedEntry"),
CreateGenerationEntry("anotherTemplate", null, 0, "NamedEntry")
});
// Default values for parameters are taken into account by comparing the templates.
data.Add(new[]
{
CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"),
CreateGenerationEntry("template/{parameter=1}", null, 0, "NamedEntry"),
CreateGenerationEntry("template/{parameter=2}", null, 0, "NamedEntry")
});
// Names for entries are compared ignoring casing.
data.Add(new[]
{
CreateGenerationEntry("template/{*parameter:int=0}", null, 0, "NamedEntry"),
CreateGenerationEntry("template/{*parameter:int=1}", null, 0, "NAMEDENTRY"),
CreateGenerationEntry("template/{*parameter:int=2}", null, 0, "namedentry")
});
return data;
}
}
[Theory]
[MemberData(nameof(AttributeRouteTest.NamedEntriesWithDifferentTemplates))]
public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName(
IEnumerable<AttributeRouteLinkGenerationEntry> namedEntries)
{
// Arrange
string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." +
Environment.NewLine +
"Parameter name: linkGenerationEntries";
var next = new Mock<IRouter>().Object;
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
// Act
var exception = Assert.Throws<ArgumentException>(
"linkGenerationEntries",
() => new AttributeRoute(
next,
matchingEntries,
namedEntries,
NullLoggerFactory.Instance));
Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase);
}
public static IEnumerable<object[]> NamedEntriesWithTheSameTemplate
{
get
{
var data = new TheoryData<IEnumerable<AttributeRouteLinkGenerationEntry>>();
data.Add(new[]
{
CreateGenerationEntry("template", null, 0, "NamedEntry"),
CreateGenerationEntry("template", null, 1, "NamedEntry"),
CreateGenerationEntry("template", null, 2, "NamedEntry")
});
// Templates are compared ignoring casing.
data.Add(new[]
{
CreateGenerationEntry("template", null, 0, "NamedEntry"),
CreateGenerationEntry("Template", null, 1, "NamedEntry"),
CreateGenerationEntry("TEMPLATE", null, 2, "NamedEntry")
});
data.Add(new[]
{
CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"),
CreateGenerationEntry("template/{parameter=0}", null, 1, "NamedEntry"),
CreateGenerationEntry("template/{parameter=0}", null, 2, "NamedEntry")
});
return data;
}
}
[Theory]
[MemberData(nameof(AttributeRouteTest.NamedEntriesWithTheSameTemplate))]
public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate(
IEnumerable<AttributeRouteLinkGenerationEntry> namedEntries)
{
// Arrange
var expectedLink = namedEntries.First().Template.Parameters.Any() ? "template/5" : "template";
var expectedGroup = "0&" + namedEntries.First().TemplateText;
string selectedGroup = null;
var next = new Mock<IRouter>();
next.Setup(s => s.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(vpc =>
{
vpc.IsBound = true;
selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey];
});
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
var route = new AttributeRoute(
next.Object,
matchingEntries,
namedEntries,
NullLoggerFactory.Instance);
var ambientValues = namedEntries.First().Template.Parameters.Any() ? new { parameter = 5 } : null;
var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedEntry");
// Act
var result = route.GetVirtualPath(context);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedGroup, selectedGroup);
Assert.Equal(expectedLink, result);
}
[Fact]
public void AttributeRoute_GenerateLink_WithName()
{
// Arrange
string selectedGroup = null;
var next = new Mock<IRouter>();
next.Setup(s => s.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(vpc =>
{
vpc.IsBound = true;
selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey];
});
var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute");
var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0);
// The named route has a lower order which will ensure that we aren't trying the route as
// if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance);
var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute");
// Act
var result = route.GetVirtualPath(context);
// Assert
Assert.NotNull(result);
Assert.Equal("1&named", selectedGroup);
Assert.Equal("named", result);
}
[Fact]
public void AttributeRoute_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName()
{
// Arrange
string selectedGroup = null;
var next = new Mock<IRouter>();
next.Setup(s => s.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(vpc =>
{
vpc.IsBound = true;
selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey];
});
var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute");
// Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0);
// The named route has a lower order which will ensure that we aren't trying the route as
// if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance);
var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute");
// Act
var result = route.GetVirtualPath(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("template/{parameter:int}", null)]
[InlineData("template/{parameter:int}", "NaN")]
[InlineData("template/{parameter}", null)]
[InlineData("template/{*parameter:int}", null)]
[InlineData("template/{*parameter:int}", "NaN")]
public void AttributeRoute_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value)
{
// Arrange
string selectedGroup = null;
var next = new Mock<IRouter>();
next.Setup(s => s.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(vpc =>
{
vpc.IsBound = true;
selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey];
});
var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute");
// Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0);
// The named route has a lower order which will ensure that we aren't trying the route as
// if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance);
var ambientValues = value == null ? null : new { parameter = value };
var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute");
// Act
var result = route.GetVirtualPath(context);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("template/{parameter:int}", "5")]
[InlineData("template/{parameter}", "5")]
[InlineData("template/{*parameter:int}", "5")]
[InlineData("template/{*parameter}", "5")]
public void AttributeRoute_GeneratesLink_IfValuesMatchNamedEntry(string template, string value)
{
// Arrange
string selectedGroup = null;
var next = new Mock<IRouter>();
next.Setup(s => s.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(vpc =>
{
vpc.IsBound = true;
selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey];
});
var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute");
// Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0);
// The named route has a lower order which will ensure that we aren't trying the route as
// if it were an unnamed route.
var linkGenerationEntries = new[] { namedEntry, unnamedEntry };
var matchingEntries = Enumerable.Empty<AttributeRouteMatchingEntry>();
var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance);
var ambientValues = value == null ? null : new { parameter = value };
var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute");
// Act
var result = route.GetVirtualPath(context);
// Assert
Assert.NotNull(result);
Assert.Equal(string.Format("1&{0}", template), selectedGroup);
Assert.Equal("template/5", result);
}
[Fact]
public async void AttributeRoute_RouteAsyncHandled_LogsCorrectValues()
{
@ -843,7 +1126,10 @@ namespace Microsoft.AspNet.Mvc.Routing
return new RouteContext(context.Object);
}
private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues = null)
private static VirtualPathContext CreateVirtualPathContext(
object values,
object ambientValues = null,
string name = null)
{
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory)))
@ -852,7 +1138,8 @@ namespace Microsoft.AspNet.Mvc.Routing
return new VirtualPathContext(
mockHttpContext.Object,
new RouteValueDictionary(ambientValues),
new RouteValueDictionary(values));
new RouteValueDictionary(values),
name);
}
private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order)
@ -872,7 +1159,11 @@ namespace Microsoft.AspNet.Mvc.Routing
return entry;
}
private static AttributeRouteLinkGenerationEntry CreateGenerationEntry(string template, object requiredValues, int order = 0)
private static AttributeRouteLinkGenerationEntry CreateGenerationEntry(
string template,
object requiredValues,
int order = 0,
string name = null)
{
var constraintResolver = CreateConstraintResolver();
@ -895,7 +1186,7 @@ namespace Microsoft.AspNet.Mvc.Routing
entry.Precedence = AttributeRoutePrecedence.Compute(entry.Template);
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
entry.RouteGroup = CreateRouteGroup(order, template);
entry.Name = name;
return entry;
}

View File

@ -529,6 +529,124 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("/", result.Link);
}
[Theory]
[InlineData("GET", "Get")]
[InlineData("PUT", "Put")]
public async Task AttributeRoutedAction_LinkWithName_WithNameInheritedFromControllerRoute(string method, string actionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.SendAsync(method, "http://localhost/api/Company/5");
Assert.Equal(200, response.StatusCode);
// Assert
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("Company", result.Controller);
Assert.Equal(actionName, result.Action);
Assert.Equal("/api/Company/5", result.ExpectedUrls.Single());
Assert.Equal("Company", result.RouteName);
}
[Fact]
public async Task AttributeRoutedAction_LinkWithName_WithNameOverrridenFromController()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.SendAsync("DELETE", "http://localhost/api/Company/5");
Assert.Equal(200, response.StatusCode);
// Assert
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("Company", result.Controller);
Assert.Equal("Delete", result.Action);
Assert.Equal("/api/Company/5", result.ExpectedUrls.Single());
Assert.Equal("RemoveCompany", result.RouteName);
}
[Fact]
public async Task AttributeRoutedAction_Link_WithNonEmptyActionRouteTemplateAndNoActionRouteName()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
var url = LinkFrom("http://localhost")
.To(new { id = 5 });
// Act
var response = await client.SendAsync("GET", "http://localhost/api/Company/5/Employees");
Assert.Equal(200, response.StatusCode);
// Assert
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("Company", result.Controller);
Assert.Equal("GetEmployees", result.Action);
Assert.Equal("/api/Company/5/Employees", result.ExpectedUrls.Single());
Assert.Equal(null, result.RouteName);
}
[Fact]
public async Task AttributeRoutedAction_LinkWithName_WithNonEmptyActionRouteTemplateAndActionRouteName()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.SendAsync("GET", "http://localhost/api/Company/5/Departments");
Assert.Equal(200, response.StatusCode);
// Assert
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("Company", result.Controller);
Assert.Equal("GetDepartments", result.Action);
Assert.Equal("/api/Company/5/Departments", result.ExpectedUrls.Single());
Assert.Equal("Departments", result.RouteName);
}
[Theory]
[InlineData("http://localhost/Duplicate/Index")]
[InlineData("http://localhost/api/Duplicate/IndexAttribute")]
[InlineData("http://localhost/api/Duplicate")]
[InlineData("http://localhost/conventional/Duplicate")]
public async Task AttributeRoutedAction_ThowsIfConventionalRouteWithTheSameName(string url)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
var expectedMessage = "The supplied route name 'DuplicateRoute' is ambiguous and matched more than one route.";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await client.SendAsync("GET", url));
// Assert
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public async Task ConventionalRoutedAction_LinkToArea()
{
@ -869,7 +987,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.Handler;
// Act
var url =
var url =
LinkFrom("http://localhost/")
.To(new { action = "GetProducts", controller = "Products", country = "US" });
var response = await client.GetAsync(url);
@ -935,6 +1053,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
public Dictionary<string, object> RouteValues { get; set; }
public string RouteName { get; set; }
public string Action { get; set; }
public string Controller { get; set; }

View File

@ -0,0 +1,63 @@
// 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
{
// A controller can define a route for all of the actions
// in it and give it a name for link generation purposes.
[Route("api/Company/{id}", Name = "Company")]
public class CompanyController : Controller
{
private readonly TestResponseGenerator _generator;
public CompanyController(TestResponseGenerator generator)
{
_generator = generator;
}
// An action with the same template will inherit the name
// from the controller.
[HttpGet]
public ActionResult Get(int id)
{
return _generator.Generate(Url.RouteUrl("Company", new { id = id }));
}
// Multiple actions can have the same named route as long
// as for a given Name, all the actions have the same template.
// That is, there can't be two link generation entries with same
// name and different templates.
[HttpPut]
public ActionResult Put(int id)
{
return _generator.Generate(Url.RouteUrl("Company", new { id = id }));
}
// Two actions can have the same template and each of them can have
// a different route name. That is, a given template can have multiple
// names associated with it.
[HttpDelete(Name = "RemoveCompany")]
public ActionResult Delete(int id)
{
return _generator.Generate(Url.RouteUrl("RemoveCompany", new { id = id }));
}
// An action that defines a non empty template doesn't inherit the name
// from the route on the controller .
[HttpGet("Employees")]
public ActionResult GetEmployees(int id)
{
return _generator.Generate(Url.RouteUrl(new { id = id }));
}
// An action that defines a non empty template doesn't inherit the name
// from the controller but can perfectly define its own name.
[HttpGet("Departments", Name = "Departments")]
public ActionResult GetDepartments(int id)
{
return _generator.Generate(Url.RouteUrl("Departments", new { id = id }));
}
}
}

View File

@ -0,0 +1,41 @@
// 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;
using System;
namespace RoutingWebSite
{
public class DuplicateController : Controller
{
private readonly TestResponseGenerator _generator;
public DuplicateController(TestResponseGenerator generator)
{
_generator = generator;
}
[HttpGet("api/Duplicate/", Name = "DuplicateRoute")]
public ActionResult DuplicateAttribute()
{
return _generator.Generate(Url.RouteUrl("DuplicateRoute"));
}
[HttpGet("api/Duplicate/IndexAttribute")]
public ActionResult IndexAttribute()
{
return _generator.Generate(Url.RouteUrl("DuplicateRoute"));
}
[HttpGet]
public ActionResult Duplicate()
{
return _generator.Generate(Url.RouteUrl("DuplicateRoute"));
}
public ActionResult Index()
{
return _generator.Generate(Url.RouteUrl("DuplicateRoute"));
}
}
}

View File

@ -29,5 +29,8 @@ namespace RoutingWebSite
/// <inheritdoc />
public int? Order { get; set; }
/// <inheritdoc />
public string Name { get; set; }
}
}

View File

@ -33,6 +33,15 @@ namespace RoutingWebSite
"products",
"api/Products/{country}/{action}",
defaults: new { controller = "Products" });
// Added this route to validate that we throw an exception when a conventional
// route matches a link generated by a named attribute route.
// The conventional route will match first, but when the attribute route generates
// a valid route an exception will be thrown.
routes.MapRoute(
"DuplicateRoute",
"conventional/Duplicate",
defaults: new { controller = "Duplicate", action = "Duplicate" });
});
}
}

View File

@ -37,10 +37,13 @@ namespace RoutingWebSite
link = urlHelper.Action(query["link_action"], query["link_controller"], values);
}
var attributeRoutingInfo = _actionContext.ActionDescriptor.AttributeRouteInfo;
return new JsonResult(new
{
expectedUrls = expectedUrls,
actualUrl = _actionContext.HttpContext.Request.Path.Value,
routeName = attributeRoutingInfo == null ? null : attributeRoutingInfo.Name,
routeValues = new Dictionary<string, object>(_actionContext.RouteData.Values),
action = _actionContext.ActionDescriptor.Name,