Generating links using expressions on ASP.NET Web API 2

On Web API the most common way to generate links is using the Link or Route methods of the UrlHelper class. Both accept a route name and the corresponding route values. Let’s consider a simple example with two controllers using attribute routing

public class CustomersController : ApiController
{
   [Route("customers/{id}", Name = "GetCustomerById")]
   public HttpResponseMessage Get(int id){ ... }
}

public class OrdersController : ApiController
{
   [Route("customers/{customerId}/orders", Name = "GetOrdersForCustomer")]
   public HttpResponseMessage GetOrdersForCustomer(int customerId){ ... }

   [Route("orders/{id}", Name = "GetById")]
   public HttpResponseMessage GetById(int id){ ... }
}

To generate a link for a customer’s orders one would write:

this.Url.Link("GetOrdersForCustomer", new { customerId = id })

Despite being sufficient, this approach does not allow for some compile-time safety. This means you’ll need to know the exact name and type of the route parameters everywhere you use it. Furthermore, if you change the name or number of the parameters, you’ll have to be careful not to break the links being generated.

A more convenient approach is to use expressions to represent these links and write a few extension methods to replace the existing Link and Route methods. For instance, when generating a link to an order from the OrdersController on the previous exemple, one would write:

var link = this.Link(c => c.GetById(id))

When generating cross-controller links, one would write:

var link = this.Url.Link<OrdersController>(c => c.GetOrdersForCustomer(id))

With the approach on these examples one doesn’t need to worry about route names nor parameter names when generating links. More important, we get compile-time checks. Let’s focus on the second example, as the first one is just a convenient wrapper. The extension method’s signature is the following:

public static string Link<T>(this UrlHelper url, Expression<Action<T>> actionExpr)

The expression must be a MethodCallExpression. whose target method is the method that corresponds to the intended route on a given controller. In addition, the target method should be annotated with a Route custom attribute where the route name is specified. To generate the final link the extension method performs the following steps:

  • Get the Route from the expression’s method:
var methodCall = actionExpr.Body as MethodCallExpression;
var actionMethod = methodCall.Method;
var route = actionMethod
  .GetCustomAttributes(typeof(RouteAttribute), true)
  .Cast()
  .FirstOrDefault(r => !String.IsNullOrEmpty(r.Name));
  • Get the route values. These values are supplied as part of the expression arguments, which are also expressions. These means we’ll need to evaluate these expressions. Instead of evaluating one by one, we can produce a dictionary of route values. The code is the expression-equivalent of using a collection initializer, but wrapped on a lambda expression so that we can later compile it:
var actionParameters = actionMethod.GetParameters();
var routeValuesExpr = Expression.Lambda<Func<Dictionary<string, object>>>
(
  Expression.ListInit(
     Expression.New(typeof(Dictionary<string, object>)),
     methodCall.Arguments.Select((a, i) => Expression.ElementInit(
        DictionaryAdd,
        Expression.Constant(actionParameters[i].Name),
        Expression.Convert(a, typeof(object))))
	)
);

The route parameter names are obtained from the target method’s metadata and their values are the original argument expressions (wrapped in a cast to ensure value-type boxing).

  • The last step is to compile the route values expression and execute the resulting delegate so that we get the route values dictionary. Then, we just need to generate the link using the available UrlHelper methods.
var routeValuesGetter = routeValuesExpr.Compile();
var routeValues = routeValuesGetter();
return url.Link(route.Name, routeValues);

The complete code of the Link extensions method goes bellow.

And that’s it. Hope it helps!

public static class UrlHelperExtensions
{
  private static readonly MethodInfo DictionaryAdd;

  static UrlHelperExtensions()
  {
    Expression<Action<Dictionary<string, object>>> method = d => d.Add(String.Empty, null);
    DictionaryAdd = ((MethodCallExpression)method.Body).Method;
  }

  public static string Link(this UrlHelper url, Expression<Action> actionExpr)
  {
    var methodCall = actionExpr.Body as MethodCallExpression;
    if (methodCall == null)
    {
      throw new ArgumentException("The expression must be a method call", "actionExpr");
    }

    var actionMethod = methodCall.Method;
    var actionParameters = actionMethod.GetParameters();

    var route = actionMethod.GetCustomAttributes(typeof(RouteAttribute), true)
      .Cast()
      .FirstOrDefault(r => !String.IsNullOrEmpty(r.Name));
    if(route == null)
    {
      throw new InvalidOperationException("Action method must have a named Route attribute");
    }

    var routeValuesExpr = Expression.Lambda<Func<Dictionary<string, object>>>
    (
      Expression.ListInit(
        Expression.New(typeof(Dictionary<string, object>)),
        methodCall.Arguments.Select((a, i) => Expression.ElementInit(
            DictionaryAdd,
            Expression.Constant(actionParameters[i].Name),
            Expression.Convert(a, typeof(object))))
      )
    );

    var routeValuesGetter = routeValuesExpr.Compile();
    var routeValues = routeValuesGetter();

    return url.Link(route.Name, routeValues);
  }
}
Advertisement