Click here to Skip to main content
15,887,264 members
Articles / Web Development / ASP.NET
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
4.56/5 (6 votes)
3 Nov 2014CPOL3 min read 33.8K   88   10   16
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Germany Germany
I'm an IT freelancer.

Comments and Discussions

 
QuestionNuget Pin
win-nie4-Aug-15 0:03
win-nie4-Aug-15 0:03 
AnswerRe: Nuget Pin
Jens Theisen4-Aug-15 0:30
Jens Theisen4-Aug-15 0:30 
Questionas would be the implementation for routes with areas? Pin
Flávio Henrique de Carvalho12-Nov-14 0:57
professionalFlávio Henrique de Carvalho12-Nov-14 0:57 
AnswerRe: as would be the implementation for routes with areas? Pin
Jens Theisen12-Nov-14 1:57
Jens Theisen12-Nov-14 1:57 
AnswerRe: as would be the implementation for routes with areas? Pin
Jens Theisen12-Nov-14 3:40
Jens Theisen12-Nov-14 3:40 
QuestionAlternative solutions Pin
Vahid_N3-Nov-14 22:43
Vahid_N3-Nov-14 22:43 
AnswerRe: Alternative solutions Pin
Jens Theisen3-Nov-14 23:05
Jens Theisen3-Nov-14 23:05 
GeneralRe: Alternative solutions Pin
Vahid_N3-Nov-14 23:18
Vahid_N3-Nov-14 23:18 
GeneralRe: Alternative solutions Pin
Jens Theisen3-Nov-14 23:59
Jens Theisen3-Nov-14 23:59 
AnswerRe: Alternative solutions Pin
Jens Theisen9-Nov-14 22:23
Jens Theisen9-Nov-14 22:23 
QuestionHow to resolve problem for route of dynamic paramater Pin
wellxion3-Nov-14 21:10
wellxion3-Nov-14 21:10 
AnswerRe: How to resolve problem for route of dynamic paramater Pin
Jens Theisen3-Nov-14 21:32
Jens Theisen3-Nov-14 21:32 
AnswerRe: How to resolve problem for route of dynamic paramater Pin
Jens Theisen3-Nov-14 22:29
Jens Theisen3-Nov-14 22:29 
GeneralRe: How to resolve problem for route of dynamic paramater Pin
wellxion3-Nov-14 22:45
wellxion3-Nov-14 22:45 
AnswerRe: How to resolve problem for route of dynamic paramater Pin
Jens Theisen9-Nov-14 22:25
Jens Theisen9-Nov-14 22:25 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun3-Nov-14 17:13
Humayun Kabir Mamun3-Nov-14 17:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.