Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Yet Again: Safe URL Creation with ASP.NET MVC

0.00/5 (No votes)
9 Nov 2014 1  
A better approach to name- and type-safe urls.

Introduction

Just recently I wrote a tip on how to create type- and name-safe urls to action methods in ASP.NET MVC. Feedback and further thought has convinced me that my solution was worse than already existing approaches (T4MVC). But I can do better:

<a href="@(Url.To<MyController>().MyAction(myString: "foo", myInt: 42))">link</a>

My former suggestion was syntax-tree based and couldn't support named arguments because named arguments can't be used in lambdas - an unfortunate C# weakness.

As you can see, there are no lambdas involved in the new syntax and so named arguments work.

This time, I also created a project on GitHub and a package on NuGet.

Using the code

There's only one set of generic overloads named To and it's on the UrlHelper class, reachable as Url in both view and controller classes.

If you use the NuGet package there are some installation steps you should follow that aren't specific to this helper but rather steps necessary to get any third-party dll visible in views with proper intellisense - I just refer to the project site for that. In this tip, we're going straight at the usage.

In a view that is:

<a href="@(Url.To<MyController>().MyAction(myString: "foo", myInt: 42))">link</a>

(make sure that you have the parentheses match before expecting any intellisense from VS 2013)

And an example for a usage within a controller would be:

return Redirect(Url.To<MyController>().MyAction("foo", 42).ToString());

Note the .ToString() on the result of the action call. This is necessary is because the return type of the action is, of course, not String - or even Url - but rather whatever the action in question returned. ActionResult for example.

However, the specific controller the action is called on - the one returned by Url.To<MyController>() - isn't your normal controller. It is one that has all it's action implementations hidden by overrides that don't actually execute any actions but rather return URLs to them - hackily disguised in fake ActionsResults. Or fake Task<ActionResult>s.

The attempt to actually evaluate those results in the way a controller action's results are normally evaluate result in an exception. The only meaningful thing you can do with them is to call .ToString() on them. Within views, that call is implicitly done for us. Nice.

I know that this sounds a bit scary at first, but I like to point out that in fact T4MVC does it too:

MVC.MyControllerName.MyAction()

actually returns a T4MVC_System_Web_Mvc_ActionResult which derives from ActionResult.

That's why in both solutions, all of your action methods must be virtual and there are certain limitations on the actions' return values.

In my solution's case, the limitations are a bit stricter than in T4MVC's case: the return values have to be ActionResult or Task<ActionResult>. Trying to use .To<C>() on anything else will get you in trouble with my controller nanny, a safety measure that will tell you what's wrong and prevents you from accidently executing any actions you merely wanted a URL to. (The nanny won't complain about void results, but you still can't create any URLs to such actions.)

Now let's look at the main differences of my own solution to T4MVC:

  • My "fake results" can be stringified (they implement .ToString()) and so can be used in views directly, without any additional helpers.
  • My derived controller class is created at runtime with the help of Castle's Dynamic Proxy, not at build time with T4.

There's also one small other design decision I made different that T4MVC - a URI will, by default, not contain query arguments that do not change the defaults: A definition of

public class GoodController : Controller {
    public virtual ActionResult WithDefaultString(String s = "default") { return View(); } }

gives those respective results (taken from the unit tests):

AssertEqual(Url.To<GoodController>().WithDefaultString("foo"), "/Good/WithDefaultString?s=foo");
AssertEqual(Url.To<GoodController>().WithDefaultString("default"), "/Good/WithDefaultString");
AssertEqual(Url.To<GoodController>().WithDefaultString(), "/Good/WithDefaultString");

I think this is a sensible default behavior and leads to less cluttered urls. You can always specific additional route values - those will not be dropped when equal to a default:

AssertEqual( Url.To<GoodController>(new { s = "default" }).WithDefaultString(),
             "/Good/WithDefaultString?s=default" );

Another, smaller, difference is that I deliberatly don't provide any other overloads, especially not on HtmlHelper. In the spirit of seperation of concerns, this helper creates urls - it has nothing to do with html. I think adding overloads for creating action links or controller redirects don't really add enough value to justify doing it. The package gives you .To<C>(), you already know how to do the rest once you got your URL.

Conclusion

I wouldn't have written this second version without sensible feedback from the last article. I would really appreciate to hear especially from people who used T4MVC before, and if my way of doing can compare to the other project.

I'm also interested whether people feel HtmlHelper overloads are important and whether there are people who can't live without return values other than ActionResult.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here