Wednesday, October 31, 2012

Constraint on Action names in MVC routing parameter

Let's say we have Web-site named "example.com" and controller with 3 Action methods:
 public class DiscussionController : BaseController  
 {  
      ...  
    public ActionResult One(string url)  
    {  
      var model= DiscussionService.GetDiscussionByUrl(url);  
      return View(model);  
    }  
    public ActionResult All()  
    {  
      ...  
    }  
    public ActionResult Popular()  
    {  
      ...  
    }  
 }  

We also have 2 registered routes(custom and standart MVC route):
 public static void RegisterRoutes(RouteCollection routes)  
 {  
   routes.MapRoute(  
     "Discussion",  
     "discussion/{url}",  
      new { controller = "Discussion" , action = "One" }  
    );  
   routes.MapRoute(  
     "Default",  
     "{controller}/{action}/{id}",  
     new { controller = "Home" , action = "Index" , id = "" }  
    );  
 }  

If we open "example.com/discussion/to-be-on-not-to-be/" URL in browser then One() Action method fires with url="to-be-on-not-to-be". So, our 1st route works fine.

But if we try to fire All() method with "example.com/discussion/all/" URL, we get One() method called again with url="all". Problem is that our 1st route fires before default route.

One solution is to use custom Constraint to filter names of controller's Action methods from our custom route:
 public static void RegisterRoutes(RouteCollection routes)  
 {  
    routes.MapRoute(  
      "Discussion",  
      "discussion/{url}",  
      new { controller = "Discussion" , action = "One" },   
      new { url = new ControllerMethodsConstraint<DiscussionController>("url" ) }  
     );   
     routes.MapRoute(  
       "Default",  
       "{controller}/{action}/{id}",  
       new { controller = "Home" , action = "Index" , id = "" }  
     );        
 }  

This Constraint class looks like:
 public class ControllerMethodsConstraint<T>: IRouteConstraint where T : BaseController  
 {  
    private readonly static IList<string> _occupiedMethodsNames;  
    private readonly string _idParamName;  
    static ControllerMethodsConstraint()  
    {  
      _occupiedMethodsNames = typeof(T).GetMethods().Where(m => m.IsPublic && (typeof (ActionResult ).IsAssignableFrom(m.ReturnType))).Select(m => m.Name.ToLower()).ToArray();  
    }  
    public ControllerMethodsConstraint(string idParamName)  
    {  
      _idParamName = idParamName;  
    }  
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)  
    {  
      return !_occupiedMethodsNames.Contains(((string )values[_idParamName]).ToLower());  
    }  
 }  

This Constraint actually just check if current param(in our case it is "url") value not equals to Action method names of specified controller.
If there is no Action methods with that name, then route is approved. Otherwise - not, so, next(default) route will work.

No comments:

Post a Comment