diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs
index 2928cde9e4..a7f8251811 100644
--- a/samples/MvcSample.Web/HomeController.cs
+++ b/samples/MvcSample.Web/HomeController.cs
@@ -126,6 +126,37 @@ namespace MvcSample.Web
return user;
}
+ [HttpGet("/AttributeRouting/{other}", Order = 0)]
+ public string LowerPrecedence(string param)
+ {
+ return "Lower";
+ }
+
+ // Normally this route would be tried before the one above
+ // as it is more explicit (doesn't have a parameter), but
+ // due to the fact that it has a higher order, it will be
+ // tried after the route above.
+ [HttpGet("/AttributeRouting/HigherPrecedence", Order = 1)]
+ public string HigherOrder()
+ {
+ return "Higher";
+ }
+
+ // Both routes have the same template, which would make
+ // them ambiguous, but the order we defined in the routes
+ // disambiguates them.
+ [HttpGet("/AttributeRouting/SameTemplate", Order = 0)]
+ public string SameTemplateHigherOrderPrecedence()
+ {
+ return "HigherOrderPrecedence";
+ }
+
+ [HttpGet("/AttributeRouting/SameTemplate", Order = 1)]
+ public string SameTemplateLowerOrderPrecedence()
+ {
+ return "LowerOrderPrecedence";
+ }
+
///
/// Action that exercises default view names.
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs
index 6a1fb69183..7595f906d6 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs
@@ -3,15 +3,13 @@
using System;
using System.Collections.Generic;
-using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Identifies an action that only supports the HTTP DELETE method.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ public class HttpDeleteAttribute : HttpMethodAttribute
{
private static readonly IEnumerable _supportedMethods = new string[] { "DELETE" };
@@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
/// Creates a new .
///
public HttpDeleteAttribute()
+ : base(_supportedMethods)
{
}
@@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
///
/// The route template. May not be null.
public HttpDeleteAttribute([NotNull] string template)
+ : base(_supportedMethods, template)
{
- Template = template;
}
-
- ///
- public IEnumerable HttpMethods
- {
- get { return _supportedMethods; }
- }
-
- ///
- public string Template { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
index 72c3145108..8ef6d09939 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
@@ -3,15 +3,13 @@
using System;
using System.Collections.Generic;
-using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Identifies an action that only supports the HTTP GET method.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ public class HttpGetAttribute : HttpMethodAttribute
{
private static readonly IEnumerable _supportedMethods = new string[] { "GET" };
@@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
/// Creates a new .
///
public HttpGetAttribute()
+ : base(_supportedMethods)
{
}
@@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
///
/// The route template. May not be null.
public HttpGetAttribute([NotNull] string template)
+ : base(_supportedMethods, template)
{
- Template = template;
}
-
- ///
- public IEnumerable HttpMethods
- {
- get { return _supportedMethods; }
- }
-
- ///
- public string Template { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
new file mode 100644
index 0000000000..c7d96f1074
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpMethodAttribute.cs
@@ -0,0 +1,74 @@
+// 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 System;
+using System.Collections.Generic;
+using Microsoft.AspNet.Mvc.Routing;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Identifies an action that only supports a given set of HTTP methods.
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ {
+ private int? _order;
+ private readonly IEnumerable _httpMethods;
+
+ ///
+ /// Creates a new with the given
+ /// set of HTTP methods.
+ /// The set of supported HTTP methods.
+ ///
+ public HttpMethodAttribute([NotNull] IEnumerable httpMethods)
+ : this(httpMethods, null)
+ {
+ }
+
+ ///
+ /// Creates a new with the given
+ /// set of HTTP methods an the given route template.
+ ///
+ /// The set of supported methods.
+ /// The route template. May not be null.
+ public HttpMethodAttribute([NotNull] IEnumerable httpMethods, string template)
+ {
+ _httpMethods = httpMethods;
+ Template = template;
+ }
+
+ ///
+ public IEnumerable HttpMethods
+ {
+ get
+ {
+ return _httpMethods;
+ }
+ }
+
+ ///
+ public string Template { get; private set; }
+
+ ///
+ /// Gets the route order. The order determines the order of route execution. Routes with a lower
+ /// order value are tried first. When a route doesn't specify a value, it gets the value of the
+ /// or a default value of 0 if the
+ /// doesn't define a value on the controller.
+ ///
+ public int Order
+ {
+ get { return _order ?? 0; }
+ set { _order = value; }
+ }
+
+ ///
+ int? IRouteTemplateProvider.Order
+ {
+ get
+ {
+ return _order;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs
index 4710ff29b7..143b361d7e 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs
@@ -3,15 +3,13 @@
using System;
using System.Collections.Generic;
-using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Identifies an action that only supports the HTTP PATCH method.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public class HttpPatchAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ public class HttpPatchAttribute : HttpMethodAttribute
{
private static readonly IEnumerable _supportedMethods = new string[] { "PATCH" };
@@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
/// Creates a new .
///
public HttpPatchAttribute()
+ : base(_supportedMethods)
{
}
@@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
///
/// The route template. May not be null.
public HttpPatchAttribute([NotNull] string template)
+ : base(_supportedMethods, template)
{
- Template = template;
}
-
- ///
- public IEnumerable HttpMethods
- {
- get { return _supportedMethods; }
- }
-
- ///
- public string Template { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs
index 5a771992d4..3a06c25aa8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs
@@ -3,23 +3,21 @@
using System;
using System.Collections.Generic;
-using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Identifies an action that only supports the HTTP POST method.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public class HttpPostAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ public class HttpPostAttribute : HttpMethodAttribute
{
private static readonly IEnumerable _supportedMethods = new string[] { "POST" };
///
/// Creates a new .
///
- /// The route template. May not be null.
public HttpPostAttribute()
+ : base(_supportedMethods)
{
}
@@ -28,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
///
/// The route template. May not be null.
public HttpPostAttribute([NotNull] string template)
+ : base(_supportedMethods, template)
{
- Template = template;
}
-
- ///
- public IEnumerable HttpMethods
- {
- get { return _supportedMethods; }
- }
-
- ///
- public string Template { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs
index 9ef79a1bb4..c1beff4256 100644
--- a/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs
@@ -3,15 +3,13 @@
using System;
using System.Collections.Generic;
-using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
///
/// Identifies an action that only supports the HTTP PUT method.
///
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
- public class HttpPutAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
+ public class HttpPutAttribute : HttpMethodAttribute
{
private static readonly IEnumerable _supportedMethods = new string[] { "PUT" };
@@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
/// Creates a new .
///
public HttpPutAttribute()
+ : base(_supportedMethods)
{
}
@@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
///
/// The route template. May not be null.
public HttpPutAttribute([NotNull] string template)
+ : base(_supportedMethods, template)
{
- Template = template;
}
-
- ///
- public IEnumerable HttpMethods
- {
- get { return _supportedMethods; }
- }
-
- ///
- public string Template { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
index 46a286d607..208f9ec964 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
+++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
@@ -31,6 +31,7 @@
+
@@ -190,6 +191,7 @@
+
@@ -247,10 +249,10 @@
+
-
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
index 0d9eb23a51..e7d3e3cd62 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
@@ -17,8 +17,16 @@ namespace Microsoft.AspNet.Mvc
{
public class ReflectedActionDescriptorProvider : IActionDescriptorProvider
{
+ ///
+ /// Represents the default order associated with this provider for dependency injection
+ /// purposes.
+ ///
public static readonly int DefaultOrder = 0;
+ // This is the default order for attribute routes whose order calculated from
+ // the reflected model is null.
+ private const int DefaultAttributeRouteOrder = 0;
+
private readonly IControllerAssemblyProvider _controllerAssemblyProvider;
private readonly IActionDiscoveryConventions _conventions;
private readonly IEnumerable _globalFilters;
@@ -114,7 +122,7 @@ namespace Microsoft.AspNet.Mvc
{
var actions = new List();
- var routeGroupsByTemplate = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var hasAttributeRoutes = false;
var removalConstraints = new HashSet(StringComparer.OrdinalIgnoreCase);
var routeTemplateErrors = new List();
@@ -152,7 +160,8 @@ namespace Microsoft.AspNet.Mvc
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
{
- Template = combinedRoute.Template
+ Template = combinedRoute.Template,
+ Order = combinedRoute.Order ?? DefaultAttributeRouteOrder
};
var actionDescriptor = new ReflectedActionDescriptor()
@@ -215,6 +224,7 @@ namespace Microsoft.AspNet.Mvc
if (actionDescriptor.AttributeRouteInfo != null &&
actionDescriptor.AttributeRouteInfo.Template != null)
{
+ hasAttributeRoutes = true;
// An attribute routed action will ignore conventional routed constraints. We still
// want to provide these values as ambient values.
foreach (var constraint in actionDescriptor.RouteConstraints)
@@ -243,19 +253,14 @@ namespace Microsoft.AspNet.Mvc
actionDescriptor.AttributeRouteInfo.Template = templateText;
- // An attribute routed action is matched by its 'route group' which identifies all equivalent
- // actions.
- string routeGroup;
- if (!routeGroupsByTemplate.TryGetValue(templateText, out routeGroup))
- {
- routeGroup = GetRouteGroup(templateText);
- routeGroupsByTemplate.Add(templateText, routeGroup);
- }
+ var routeGroupValue = GetRouteGroupValue(
+ actionDescriptor.AttributeRouteInfo.Order,
+ templateText);
var routeConstraints = new List();
routeConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
- routeGroup));
+ routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
}
@@ -279,7 +284,7 @@ namespace Microsoft.AspNet.Mvc
{
// Any any attribute routes are in use, then non-attribute-routed ADs can't be selected
// when a route group returned by the route.
- if (routeGroupsByTemplate.Any())
+ if (hasAttributeRoutes)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
@@ -320,10 +325,10 @@ namespace Microsoft.AspNet.Mvc
return actions;
}
- // Returns a unique, stable key per-route-template (OrdinalIgnoreCase)
- private static string GetRouteGroup(string template)
+ private static string GetRouteGroupValue(int order, string template)
{
- return ("__route__" + template).ToUpperInvariant();
+ var group = string.Format("{0}-{1}", order, template);
+ return ("__route__" + group).ToUpperInvariant();
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
index 875677a030..a58f46e490 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedAttributeRouteModel.cs
@@ -20,14 +20,17 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public ReflectedAttributeRouteModel([NotNull] IRouteTemplateProvider templateProvider)
{
Template = templateProvider.Template;
+ Order = templateProvider.Order;
}
public string Template { get; set; }
- ///
+ public int? Order { get; set; }
+
+ ///
/// Combines two instances and returns
/// a new instance with the result.
- ///
+ ///
/// The left .
/// The right .
/// A new instance of that represents the
@@ -37,30 +40,31 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
ReflectedAttributeRouteModel left,
ReflectedAttributeRouteModel right)
{
- left = left ?? _default;
right = right ?? _default;
- var template = CombineTemplates(left.Template, right.Template);
+ // If the right template is an override template (starts with / or ~/)
+ // we ignore the values from left.
+ if (left == null || IsOverridePattern(right.Template))
+ {
+ left = _default;
+ }
+
+ var combinedTemplate = CombineTemplates(left.Template, right.Template);
// The action is not attribute routed.
- if (template == null)
+ if (combinedTemplate == null)
{
return null;
}
return new ReflectedAttributeRouteModel()
- {
- Template = template
+ {
+ Template = combinedTemplate,
+ Order = right.Order ?? left.Order
};
}
- ///
- /// Combines attribute routing templates.
- ///
- /// The left template.
- /// The right template.
- /// A combined template.
- public static string CombineTemplates(string left, string right)
+ internal static string CombineTemplates(string left, string right)
{
var result = CombineCore(left, right);
return CleanTemplate(result);
@@ -72,19 +76,11 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
return null;
}
- else if (left == null)
- {
- return right;
- }
else if (right == null)
{
return left;
}
-
- if (right.StartsWith("~/", StringComparison.OrdinalIgnoreCase) ||
- right.StartsWith("/", StringComparison.OrdinalIgnoreCase) ||
- left.Equals("~/", StringComparison.OrdinalIgnoreCase) ||
- left.Equals("/", StringComparison.OrdinalIgnoreCase))
+ else if (IsEmptyLeftSegment(left) || IsOverridePattern(right))
{
return right;
}
@@ -98,6 +94,21 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
return left + '/' + right;
}
+ private static bool IsOverridePattern(string template)
+ {
+ return template != null &&
+ (template.StartsWith("~/", StringComparison.OrdinalIgnoreCase) ||
+ template.StartsWith("/", StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static bool IsEmptyLeftSegment(string template)
+ {
+ return template == null ||
+ template.Equals(string.Empty, StringComparison.OrdinalIgnoreCase) ||
+ template.Equals("~/", StringComparison.OrdinalIgnoreCase) ||
+ template.Equals("/", StringComparison.OrdinalIgnoreCase);
+ }
+
private static string CleanTemplate(string result)
{
if (result == null)
diff --git a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
index b27fd64df4..138215762f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
@@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Mvc
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class RouteAttribute : Attribute, IRouteTemplateProvider
{
+ private int? _order;
+
///
/// Creates a new with the given route template.
///
@@ -23,5 +25,26 @@ namespace Microsoft.AspNet.Mvc
///
public string Template { get; private set; }
+
+ ///
+ /// Gets the route order. The order determines the order of route execution. Routes with a lower order
+ /// value are tried first. If an action defines a route by providing an
+ /// with a non null order, that order is used instead of this value. If neither the action nor the
+ /// controller defines an order, a default value of 0 is used.
+ ///
+ public int Order
+ {
+ get { return _order ?? 0; }
+ set { _order = value; }
+ }
+
+ ///
+ int? IRouteTemplateProvider.Order
+ {
+ get
+ {
+ return _order;
+ }
+ }
}
}
\ 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 f4ffe7d273..a71d902924 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
@@ -29,20 +29,30 @@ namespace Microsoft.AspNet.Mvc.Routing
/// The next router. Invoked when a route entry matches.
/// The set of route entries.
public AttributeRoute(
- [NotNull] IRouter next,
+ [NotNull] IRouter next,
[NotNull] IEnumerable matchingEntries,
[NotNull] IEnumerable linkGenerationEntries,
[NotNull] ILoggerFactory factory)
{
_next = next;
- // FOR RIGHT NOW - this is just an array of regular template routes. We'll follow up by implementing
- // a good data-structure here. See #740
- _matchingRoutes = matchingEntries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
+ // Order all the entries by order, then precedence, and then finally by template in order to provide
+ // a stable routing and link generation order for templates with same order and precedence.
+ // We use ordinal comparison for the templates because we only care about them being exactly equal and
+ // we don't want to make any equivalence between templates based on the culture of the machine.
- // FOR RIGHT NOW - this is just an array of entries. We'll follow up by implementing
- // a good data-structure here. See #741
- _linkGenerationEntries = linkGenerationEntries.OrderBy(e => e.Precedence).ToArray();
+ _matchingRoutes = matchingEntries
+ .OrderBy(o => o.Order)
+ .ThenBy(e => e.Precedence)
+ .ThenBy(e => e.Route.RouteTemplate, StringComparer.Ordinal)
+ .Select(e => e.Route)
+ .ToArray();
+
+ _linkGenerationEntries = linkGenerationEntries
+ .OrderBy(o => o.Order)
+ .ThenBy(e => e.Precedence)
+ .ThenBy(e => e.TemplateText, StringComparer.Ordinal)
+ .ToArray();
_logger = factory.Create();
_constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName);
@@ -53,12 +63,12 @@ namespace Microsoft.AspNet.Mvc.Routing
{
using (_logger.BeginScope("AttributeRoute.RouteAsync"))
{
- foreach (var route in _matchingRoutes)
- {
- await route.RouteAsync(context);
+ foreach (var route in _matchingRoutes)
+ {
+ await route.RouteAsync(context);
- if (context.IsHandled)
- {
+ if (context.IsHandled)
+ {
break;
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
index f8ba92e7d9..463de8c510 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteInfo.cs
@@ -12,5 +12,12 @@ namespace Microsoft.AspNet.Mvc.Routing
/// The route template. May be null if the action has no attribute routes.
///
public string Template { get; set; }
+
+ ///
+ /// Gets the order of the route associated with this . 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; }
}
}
\ 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 f2fc7204dc..f088991513 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
@@ -28,6 +28,11 @@ namespace Microsoft.AspNet.Mvc.Routing
///
public IDictionary Defaults { get; set; }
+ ///
+ /// The order of the template.
+ ///
+ public int Order { get; set; }
+
///
/// The precedence of the template.
///
@@ -47,5 +52,10 @@ namespace Microsoft.AspNet.Mvc.Routing
/// The .
///
public RouteTemplate Template { get; set; }
+
+ ///
+ /// The original representing the route template.
+ ///
+ public string TemplateText { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
index 39790523d6..8816b3882f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
@@ -12,6 +12,11 @@ namespace Microsoft.AspNet.Mvc.Routing
///
public class AttributeRouteMatchingEntry
{
+ ///
+ /// The order of the template.
+ ///
+ public int Order { get; set; }
+
///
/// The precedence of the template.
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
index d7cbecdebb..a9922c846f 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
@@ -41,10 +41,12 @@ namespace Microsoft.AspNet.Mvc.Routing
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
Defaults = routeInfo.Defaults,
Constraints = routeInfo.Constraints,
+ Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
RouteGroup = routeInfo.RouteGroup,
Template = routeInfo.ParsedTemplate,
+ TemplateText = routeInfo.RouteTemplate
});
}
@@ -57,6 +59,7 @@ namespace Microsoft.AspNet.Mvc.Routing
{
matchingEntries.Add(new AttributeRouteMatchingEntry()
{
+ Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
Route = new TemplateRoute(
target,
@@ -72,9 +75,9 @@ namespace Microsoft.AspNet.Mvc.Routing
}
return new AttributeRoute(
- target,
- matchingEntries,
- generationEntries,
+ target,
+ matchingEntries,
+ generationEntries,
services.GetService());
}
@@ -133,11 +136,11 @@ namespace Microsoft.AspNet.Mvc.Routing
if (errors.Count > 0)
{
var allErrors = string.Join(
- Environment.NewLine + Environment.NewLine,
+ Environment.NewLine + Environment.NewLine,
errors.Select(
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
- e.ActionDescriptor.DisplayName,
- Environment.NewLine,
+ e.ActionDescriptor.DisplayName,
+ Environment.NewLine,
e.ErrorMessage)));
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
@@ -208,6 +211,8 @@ namespace Microsoft.AspNet.Mvc.Routing
}
}
+ routeInfo.Order = action.AttributeRouteInfo.Order;
+
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
routeInfo.Constraints = routeInfo.ParsedTemplate.Parameters
@@ -233,6 +238,8 @@ namespace Microsoft.AspNet.Mvc.Routing
public RouteTemplate ParsedTemplate { get; set; }
+ public int Order { get; set; }
+
public decimal Precedence { get; set; }
public string RouteGroup { get; set; }
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
index 2dc7969d2d..4cbd7ff3d1 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/IRouteTemplateProvider.cs
@@ -12,5 +12,13 @@ namespace Microsoft.AspNet.Mvc.Routing
/// The route template. May be null.
///
string Template { get; }
+
+ ///
+ /// Gets the route order. The order determines the order of route execution. Routes with a lower
+ /// order value are tried first. When a route doesn't specify a value, it gets a default value of 0.
+ /// A null value for the Order property means that the user didn't specify an explicit order for the
+ /// route.
+ ///
+ int? Order { get; }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
index 3b8eddb6e7..8040731a17 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
@@ -35,6 +35,7 @@
+
@@ -88,6 +89,7 @@
+
@@ -103,6 +105,7 @@
+
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
index 8f5647aa59..304b6a9a90 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedModelBuilder/ReflectedAttributeRouteModelTests.cs
@@ -101,6 +101,210 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
Assert.Equal(expected, combined);
}
+ [Theory]
+ [MemberData("ReplaceTokens_ValueValuesData")]
+ public void ReplaceTokens_ValidValues(string template, object values, string expected)
+ {
+ // Arrange
+ var valuesDictionary = values as IDictionary;
+ if (valuesDictionary == null)
+ {
+ valuesDictionary = new RouteValueDictionary(values);
+ }
+
+ // Act
+ var result = ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData("ReplaceTokens_InvalidFormatValuesData")]
+ public void ReplaceTokens_InvalidFormat(string template, object values, string reason)
+ {
+ // Arrange
+ var valuesDictionary = values as IDictionary;
+ if (valuesDictionary == null)
+ {
+ valuesDictionary = new RouteValueDictionary(values);
+ }
+
+ var expected = string.Format(
+ "The route template '{0}' has invalid syntax. {1}",
+ template,
+ reason);
+
+ // Act
+ var ex = Assert.Throws(
+ () => { ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary); });
+
+ // Assert
+ Assert.Equal(expected, ex.Message);
+ }
+
+ [Fact]
+ public void ReplaceTokens_UnknownValue()
+ {
+ // Arrange
+ var template = "[area]/[controller]/[action2]";
+ var values = new RouteValueDictionary()
+ {
+ { "area", "Help" },
+ { "controller", "Admin" },
+ { "action", "SeeUsers" },
+ };
+
+ var expected =
+ "While processing template '[area]/[controller]/[action2]', " +
+ "a replacement value for the token 'action2' could not be found. " +
+ "Available tokens: 'area, controller, action'.";
+
+ // Act
+ var ex = Assert.Throws(
+ () => { ReflectedAttributeRouteModel.ReplaceTokens(template, values); });
+
+ // Assert
+ Assert.Equal(expected, ex.Message);
+ }
+
+ [Theory]
+ [MemberData("CombineOrdersTestData")]
+ public void Combine_Orders(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right,
+ int? expected)
+ {
+ // Arrange & Act
+ var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
+
+ // Assert
+ Assert.NotNull(combined);
+ Assert.Equal(expected, combined.Order);
+ }
+
+ [Theory]
+ [MemberData("ValidReflectedAttributeRouteModelsTestData")]
+ public void Combine_ValidReflectedAttributeRouteModels(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right,
+ ReflectedAttributeRouteModel expectedResult)
+ {
+ // Arrange & Act
+ var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
+
+ // Assert
+ Assert.NotNull(combined);
+ Assert.Equal(expectedResult.Template, combined.Template);
+ }
+
+ [Theory]
+ [MemberData("NullOrNullTemplateReflectedAttributeRouteModelTestData")]
+ public void Combine_NullOrNullTemplateReflectedAttributeRouteModels(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right)
+ {
+ // Arrange & Act
+ var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
+
+ // Assert
+ Assert.Null(combined);
+ }
+
+ [Theory]
+ [MemberData("RightOverridesReflectedAttributeRouteModelTestData")]
+ public void Combine_RightOverridesReflectedAttributeRouteModel(
+ ReflectedAttributeRouteModel left,
+ ReflectedAttributeRouteModel right)
+ {
+ // Arrange
+ var expectedTemplate = ReflectedAttributeRouteModel.CombineTemplates(null, right.Template);
+
+ // Act
+ var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
+
+ // Assert
+ Assert.NotNull(combined);
+ Assert.Equal(expectedTemplate, combined.Template);
+ Assert.Equal(combined.Order, right.Order);
+ }
+
+ public static IEnumerable