Reputation: 15513
We have an IRouteConstraint
that is getting checked much more than it should. Upon further testing, it looks like that Order
on [Route]
gets ignored by route constraints.
For example, if I have the following constraint:
public class TestConstraint : IRouteConstraint {
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
) {
Debug.WriteLine("TestConstraint");
return true;
}
}
And wire it up:
constraintResolver.ConstraintMap.Add("testConstraint", typeof(TestConstraint));
And have the following routes:
public partial class HomeController {
[Route("test/0", Order = 1)]
public ActionResult Test0() {
return Content("Test0");
}
[Route("{someParam}/{test:testConstraint}", Order = 10)]
public ActionResult Test1() {
return Content("Test1");
}
}
And then make a request for http://localhost/test/0
, it will return the proper content (Test0
), but TestContraint.Match()
is still executed.
I would think that route constraints are only executed once the route is encountered in the RouteTable
, but it seems to run it on every request that can match the [Route]
pattern.
If it makes a difference, we are on ASP.NET MVC v5.2.4.
Upvotes: 3
Views: 356
Reputation: 31282
In ASP.NET MVC pipeline, the stage of routing and the stage of selection of invoked controller action are separated. On routing stage you can't just select the first matching action and stop further lookup. Found action (strictly speaking method) could be filtered on later stage. For example it may not satisfy applied action selectors (e.g. NonAction
attribute).
That's why basic action selection algorithm is the following:
Now there are following options:
If you are interested in correspondig ASP.NET MVC source code, here are some references:
IRouteConstraint.Match()
is invoked by ProcessConstraint()
method in System.Web.Routing.Route
. The nearest method in call stack, which operates on route collection level, is GetRouteData()
method in System.Web.Mvc.Routing.RouteCollectionRoute
class:
Here is its source code:
public override RouteData GetRouteData(HttpContextBase httpContext)
{
List<RouteData> matches = new List<RouteData>();
foreach (RouteBase route in _subRoutes)
{
var match = route.GetRouteData(httpContext);
if (match != null)
{
matches.Add(match);
}
}
return CreateDirectRouteMatch(this, matches);
}
As you see, the loop does not break when matching route is found.
The code that applies action selectors, performs ordering and chooses action candidate resides in DirectRouteCandidate.SelectBestCandidate()
(source code):
public static DirectRouteCandidate SelectBestCandidate(List<DirectRouteCandidate> candidates, ControllerContext controllerContext)
{
Debug.Assert(controllerContext != null);
Debug.Assert(candidates != null);
// These filters will allow actions to opt-out of execution via the provided public extensibility points.
List<DirectRouteCandidate> filteredByActionName = ApplyActionNameFilters(candidates, controllerContext);
List<DirectRouteCandidate> applicableCandidates = ApplyActionSelectors(filteredByActionName, controllerContext);
// At this point all of the remaining actions are applicable - now we're just trying to find the
// most specific match.
//
// Order is first, because it's the 'override' to our algorithm
List<DirectRouteCandidate> filteredByOrder = FilterByOrder(applicableCandidates);
List<DirectRouteCandidate> filteredByPrecedence = FilterByPrecedence(filteredByOrder);
if (filteredByPrecedence.Count == 0)
{
return null;
}
else if (filteredByPrecedence.Count == 1)
{
return filteredByPrecedence[0];
}
else
{
throw CreateAmbiguiousMatchException(candidates);
}
}
Upvotes: 3