This is the first blog in a series of blogs I am doing on stuff I learned from my current and first asp.net mvc project. I have worked on this from the first preview releases and have gone live during beta (yeah I know dangerous).

This blog is about creating a strongly typed Action Link so instead of:

 

<%=Html.ActionLink("shoes", "ShowProduct", "Product", 
    new {
    productName="Nice-Shoe", 
    categoryName="Shoes", 
    showDetails=false })%>

 

we get:

 

<%=Html.ActionLink<ProductController>("shoes", 
    c => c.ShowProduct("Nice-Shoe", "Shoes",false))%>

The first one is totally string based and not strong typed, even the last part new {productSlug=”Nice-Shoe” .. is an anonymous type so totally unchecked by the compiler.

The second solution is a strong typed Lambda based solution.

This was not supposed to be the first blog in this series because I wanted to start with something easier, but I went to an ASP.NET MVC 2 talk in the Netherlands with Scott Guthrie, I asked him about strong typed ActionLinks in 2.0 but there were none, so I offered him mine.

You can download the sample project with some unit tests here: 

  
The extensions can be found in the Helper subdirectory of the main project.

 

Ingenious secrets disclaimer (see title)

Ok, these are not secrets by definition, because I am posting them on a public website, and a real genius would never create such a title. So there you have it, I’m not one.

I was hoping to attract real geniuses, please leave comments so I can learn from you guys and improve myself.

Lambda and Generics based strong typed solution

What my solution does is, it uses generics and GetType() to figure out what the name of the controller is you are calling,

Then it analyses the lambda expression to figure out what the name of the called controller method is.

Then it evaluates the lambda expression to figure out what the values of the parameters are and uses this to generate a RouteValueDictionary

After this you just call the old fashion Html.ActionLink() method.

How, you ask?

Like this (most elaborate overload):

   1: public static string ActionLink<T>(this HtmlHelper htmlHelper, string linkText,
   2:     Expression<Func<T, ActionResult>> actionSelector,
   3:     RouteValueDictionary routeValues,
   4:     IDictionary<string, object> htmlAttributes) where T : Controller
   5: {
   6:     string controller;
   7:     string action;
   8:  
   9:     routeValues = htmlHelper.GetRouteValues(actionSelector, routeValues, out action, out controller);
  10:     return htmlHelper.ActionLink(linkText, action, controller, routeValues, htmlAttributes);
  11: }

 

 

The real magic is in GetRouteValues (below) which can be reused for other lambda to RouteValueCollection scenarios.

   1: /// <summary>
   2: /// Generates routevalues and controller and action strings from a strong typed lambda expression of a controllermethod.
   3: /// </summary>
   4: /// <typeparam name="T">Must be a Controller</typeparam>
   5: /// <param name="routeValues">Can be null or empty, but you can supply extra routevalues these win if generated values overlap</param>
   6: /// <param name="actionSelector">a lambda expression, must be a call to a ActionResult returning method</param>
   7: /// <param name="controller">the name of the controller</param>
   8: /// <param name="action">the name of the action</param>
   9: /// <returns></returns>
  10: public static RouteValueDictionary GetRouteValues<T>(
  11:     RouteValueDictionary routeValues, 
  12:     Expression<Func<T, ActionResult>> actionSelector, 
  13:     out string controller, 
  14:     out string action)
  15: {
  16:     Type controllerType = typeof(T);
  17:     if (routeValues == null)
  18:     {
  19:         routeValues = new RouteValueDictionary();
  20:     }
  21:  
  22:     //The body of the expression must be a call to a method
  23:     MethodCallExpression call = actionSelector.Body as MethodCallExpression;
  24:     if (call == null)
  25:     {
  26:         throw new ArgumentException("You must call a method of " + controllerType.Name, "actionSelector");
  27:     }
  28:  
  29:     //the object being called must be the controller specified in <T>
  30:     if (call.Object.Type != controllerType)
  31:     {
  32:         throw new ArgumentException("You must call a method of " + controllerType.Name, "actionSelector");
  33:     }
  34:  
  35:     //Remove the controller part of the name ProductController --> Product
  36:     if (controllerType.Name.EndsWith("Controller"))
  37:     {
  38:         controller = controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length);
  39:     }
  40:     else
  41:     {
  42:         controller = controllerType.Name;
  43:     }
  44:     //The action is the name of the method being called
  45:     action = call.Method.Name;
  46:  
  47:     //get all arguments from the lambda expression
  48:     var args = call.Arguments;
  49:  
  50:     //Get all parameters from the Action Method
  51:     ParameterInfo[] parameters = call.Method.GetParameters();
  52:  
  53:     //pair the lambda arguments with the param names
  54:     var pairs = args.Select((a, i) => new
  55:                                       {
  56:                                           Argument = a,
  57:                                           ParamName = parameters[i].Name
  58:                                       });
  59:  
  60:  
  61:     foreach (var argumentParameterPair in pairs)
  62:     {
  63:         string name = argumentParameterPair.ParamName;
  64:         if (!routeValues.ContainsKey(name))
  65:         {
  66:             //the argument could be a constant or a variable or a function and must be evaluated
  67:             object value;
  68:             //If it is a constant we can get the value immediately
  69:             if (argumentParameterPair.Argument.NodeType == ExpressionType.Constant)
  70:             {
  71:                 var constant = argumentParameterPair.Argument as ConstantExpression;
  72:                 value = constant.Value;
  73:             }
  74:             else //if not we have to evaluate the value
  75:             {
  76:  
  77:                 value = Expression.Lambda(argumentParameterPair.Argument).Compile().DynamicInvoke(null);
  78:             }
  79:             if (value != null)
  80:             {
  81:                 //add routevalues with the name = method parameter name (productSlug) and value = the evaluated lambda value
  82:                 routeValues.Add(name, value);
  83:             }
  84:         }
  85:     }
  86:  
  87:     return routeValues;
  88: }

Possible Issues

  • Performance, this uses reflection and dynamic lambda compiling.
    My quick test (included as a unit test?!) shows this is 3-5 times slower than the normal actionresult, maybe someone can upgrade this to cache the reflection parts or to speed up the lambda evaluation bits. (line 72 above, the Compile().DynamicInvoke(null); stuff)
  • It can’t determine the correct routevalues if you are using binding to complex objects, for example FormCollection or Model Objects in your parameter list of the action method
    It is not to difficult to write FormCollection support, but you would have to write an ModelUnbinder to support all binding.
    This is not really a big issue for me, ActionLinks are for generating links, simple Http Gets usually not complex posting of objects.
  • It only accepts methods returning ActionResult but you shouldn’t use string returning action methods anyway.

Strong Typed Url.Action

This one is now trivial. Here is the code:

   1: public static string Action<T>(this UrlHelper url,
   2:     Expression<Func<T, ActionResult>> actionSelector,
   3:     RouteValueDictionary routeValues) where T : Controller
   4: {
   5:     string controller;
   6:     string action;
   7:  
   8:     routeValues = GetRouteValues(routeValues, actionSelector, out controller, out action);
   9:     return url.Action(action, controller, routeValues);
  10: }

 

feel free to comment or improve.