Jerad Rose
Jerad Rose

Reputation: 15513

RouteAttribute's Order is completely ignored on IRouteConstraint

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

Answers (1)

CodeFuller
CodeFuller

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:

  1. Pass Request URL through configured routes and select all matching actions.
  2. Pass all matching actions through action selectors, filter out non matching.
  3. Order candidate actions by routing order.

Now there are following options:

  1. No matching actions found. Request result to 404 error.
  2. Multiple matching actions share the same highest order. Exception is thrown ("The current request is ambiguous between the following action methods ...").
  3. Exactly one matching action have highest order (or one action matches at all). The action is selected and executed.

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:

enter image description here

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

Related Questions