65.9K
CodeProject is changing. Read more.
Home

Name- and Type-safe URL Creation with ASP.NET MVC

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (6 votes)

Nov 3, 2014

CPOL

3 min read

viewsIcon

35083

downloadIcon

88

Name- and Type-safe URL Creation with ASP.NET MVC

Important: I found a better approach than the one presented in this tip and posted a new tip that supercedes this one.

Introduction

I wrote a small helper that allows a URL to be created in ASP.NET MVC with the following, name- and type-safe syntax:

Url.Action<MyController>(c => c.MyAction("somevalue", 42));

Just like the unsafe Action overloads, this gives something like the following URL, depending on the method definition and the routing configuration:

my/my?s=somevalue&n=42

If you want to use the helper, download the attached file, put it in your project, and import the namespace wherever you want UrlHelper to have the additional Action overload:

using IronStone.Web.Mvc;

Background

The standard way to define MVC URLs is to do so in a type- and name-safe way as methods ("actions") on a controller:

public class MyController : Controller
{
    public ActionResult MyAction(String s, Int32 n)
    {
        // do-stuff
    }
}

However, the standard way of creating a URL to such an action is neither name- nor type-safe at all:

Url.Action("MyAction", "MyController", new { s = "somevalue", n = 42 });

The action arguments can be provided with either an anonymous type as shown or with an instance of RouteValueDictionary, none of which provide any safety for the parameters. Even the action and controller names are taken as strings, which, in particular in the case of the controller parameter, is very puzzling.

The helper tries to improve this situation.

Caveats

While it's possible to use complex expressions within the lambda used with the helper, what unfortunately isn't possible is using named parameters:

Url.Action<MyController>(c => c.MyAction("some" + 
"value", Math.Round(42.4))); // possible
Url.Action<MyController>(c => c.MyAction(s: "somevalue", n: 42)); // not possible

This is because of a limitation of what can be expressed within C# expression trees.

(Also, look at my answers to wellxion for a pretty big caveat about using this helper with aggregate parameters.)

Implementation

The implementation relies on the usual tricks that are used to create helpers based on lambda expressions.

The extension method relies on the definition of a method taking an expression tree:

static MvcAction Get<C>(Expression<Func<C, ActionResult>> x)
    where C : IController
{

The return value of that method is a helper class that contains all the data we need in a call to the existing MVC Action overloads on the UrlHelper class:

class MvcAction
{
    public Type Controller { get; set; }
    public MethodInfo Action { get; set; }
    public RouteValueDictionary Parameter { get; set; }
}

In the Get method's definition, we first assert a call expression:

    var root = x.Body as MethodCallExpression;

    if (root == null) throw new ArgumentException("Call expression expected.");

The action name can now be found in root.MethodInfo.Name, and the controller name is simply typeof(C).Name (plus the suffix "Controller" that those classes have by convention).

The parameters are more interesting, because they might contain method calls, as in:

Url.Action("MyAction", "MyController", new { s = "some" + "value", n = Math.Round(42.4) });

To support this, we need to write a small helper to evaluate linq (sub-)expressions:

static Object Evaluate(Expression e)
{
    if (e is ConstantExpression)
    {
        return (e as ConstantExpression).Value;
    }
    else
    {
        return Expression.Lambda(e).Compile().DynamicInvoke();
    }
}

The test for a constant expression is merely optimization.

With this helper, the Get method's implementation can evaluate all the expressions in the root call expression's argument list:

for (var i = 0; i < parameters.Length; ++i)
{
    try
    {
        routeValues[parameters[i].Name] = Evaluate(arguments[i]);
    }
    catch (Exception ex)
    {
        throw new Exception(String.Format(
            "Failed to evaluate argument #{0} of an mvc action call while creating a url, "
          + "look at the inner exceptions.", i), ex);
    }
}

Each evaluation is wrapped to account for exceptions. This is to remind the programmer of why there appears to be an exception coming out of an expression evaluation - which is what it will look like when those evaluations fail at some point.

The Get method is then used to implement a new Action overload on UrlHelper that just calls one of the existing unsafe ones:

public static String Action<C>(this UrlHelper url, Expression<Func<C, ActionResult>> call)
    where C : IController
{
    var mvcAction = Get<C>(call);

    return url.Action(
        mvcAction.Action.Name,
        GetControllerName(mvcAction.Controller),
        mvcAction.Parameter
    );
}

The GetControllerName method is needed because MVC looks for the controller's type with the string "Controller" suffixed to its name:

static String GetControllerName(Type controllerType)
{
    var typeName = controllerType.Name;

    if (typeName.ToLower().EndsWith("controller"))
    {
        return typeName.Substring(0, typeName.Length - "controller".Length);
    }
    else
    {
        return typeName;
    }
}

And that's it - I hope people find this helpful.

History

This is the first version of this tip.