An Alternative Approach To Strongly Typed Helpers

0 comments suggest edit

One of the features contained in the MVC Futures project is the ability to generate action links in a strongly typed fashion using expressions. For example:

<%= Html.ActionLink<HomeController>(c => c.Index()) %>

Will generate a link to to the Index action of the HomeController.

It’s a pretty slick approach, but it is not without its drawbacks. First, the syntax is not one you’d want to take as your prom date. I guess you can get used to it, but a lot of people who see it for the first time kind of recoil at it.

The other problem with this approach is performance as seen in this slide deck I learned about from Brad Wilson. One of the pain points the authors of the deck found was that the compilation of the expressions was very slow.

I had thought that we might be able to mitigate these performance issues via some sort of caching of the compiled expressions, but that might not work very well. Consider the following case:

<% for(int i = 0; i < 20; i++) { %>

  <%= Html.ActionLink<HomeController>(c => c.Foo(i)) %>

<% } %>

Each time through that loop, the expression is the same: c => c.Foo(i)

But the value of the captured “i” is different each time. If we try to cache the compiled expression, what happens?

So I started thinking about an alternative approach using code generation against the controllers and circulated an email internally. One approach was to code gen action specific action link methods. Thus the about link for the home controller (assuming we add an id parameter for demonstration purposes) would be:

<%= HomeAboutLink(123) %>

Brad had mentioned many times that while he likes expressions, he’s no fan of using them for links and he tends to write specific action link methods just like the above. So what if we could generate them for you so you didn’t have to write them by hand?

A couple hours after starting the email thread, David Ebbo had an implementation of this ready to show off. He probably had it done earlier for all I know, I was stuck in meetings. Talk about the best kind of declarative programming. I declared what I wanted roughly with hand waving, and a little while later, the code just appears! ;)

David’s approach uses a BuildProvider to reflect over the Controllers and Actions in the solution and generate custom action link methods for each one. There’s plenty of room for improvement, such as ensuring that it honors the ActionNameAttribute and generating overloads, but it’s a neat proof of concept.

One disadvantage of this approach compared to the expression based helpers is that there’s no refactoring support. However, if you rename an action method, you will get a compilation error rather than a runtime error, which is better than what you get without either. One advantage of this approach is that it performs fast and doesn’t rely on the funky expression syntax.

These are some interesting tradeoffs we’ll be looking closely at for the next version of ASP.NET MVC.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

33 responses

  1. Avatar for Scott
    Scott June 1st, 2009

    YAY!
    Death to clever code via Expressions.

  2. Avatar for Josh Bush
    Josh Bush June 1st, 2009

    That is interesting. You might could do it with static methods on the controller. It could look something like "<%=HomeController.IndexLink(123) %>". It's still a little more verbose than what you have above though.
    I'm working on a project and I went heavily with strong typing. I really enjoy the fact that when I make changes it breaks the compilation.

  3. Avatar for Scott Allen
    Scott Allen June 1st, 2009

    I think I'd prefer the controller name as a distinct property [.Home.About(123)]. Feels more discoverable and can be easier to auto-complete.

  4. Avatar for Haacked
    Haacked June 1st, 2009

    @Josh the problem there is you'd have to write your HomeController as a partial class. We don't want to require that.
    @Scott Unfortunately, we can't generate extension properties. If we generate those as extension methods of HtmlHelper, it would look like:
    Html.Home().About(123)
    Or maybe we could add a new top level property to ViewPage.
    Link.Home().About(123)
    Or we could generate something more clever like enums
    Html.HomeLink(Action.Index, 123)
    Phil

  5. Avatar for eyston
    eyston June 1st, 2009

    I kind of like the futures way really, although I have to admit I wasn't comfortable with them when first starting MVC (which is an audience you care about).

  6. Avatar for Miha Markic
    Miha Markic June 1st, 2009
  7. Avatar for Rob Conery
    Rob Conery June 2nd, 2009

    Good stuff! Will this work in partial trust? SubSonic's BuildProvider stuff doesn't and it uses almost the exact same thing :).
    I do like this approach a LOT.

  8. Avatar for Steve Willcock
    Steve Willcock June 2nd, 2009

    Having used the mvc futures lambda syntax for a while it seems really natural and the strong typing and refactoring support is a big plus for this way of doing things. If there was a *simple* way to cache parameterised expressions to avoid the recompilation cost on every call I feel that would be a great approach. The build provider idea is undoubtedly a very clever solution to the problem but somehow it seems rather 'clunky'.

  9. Avatar for Elad Ossadon
    Elad Ossadon June 2nd, 2009

    I couldn't see this overload (with generic controller type and an expression) on Url, but only on Html. Will it be available?
    Sometimes I need the URL itself without ... (e.g. for JS).
    Of course I can work around and make a method that takes the output string and extracts the URL but this should be more elegant.
    Thanks

  10. Avatar for Paco
    Paco June 2nd, 2009

    "First, the syntax is not one you’d want to take as your prom date"
    I would definitely prefer a lamda over a a bunch of anonymous objects/strings. We are not programming in a dynamic language and emulating it with anonymous objects will never be as good as a real dynamic language. Lamda's are a very important and well-used feature of C# since 2008, so every developer have to become familiar with it.
    We use workarounds for every single string/anonymous object used in mvc since we started with mvc more than a year ago.
    We things like "return this.View<TViewType>(model)" instead of return this.View("magicstring", model). That saves us the effort writing a test per view by making the compiler doing the job.
    You might want to look at the html helpers like in mvccontrib.
    The performance of the expression based html helpers is a major disadvantage, but hardcoding the most frequently used will make it working on larger views too.

  11. Avatar for configurator
    configurator June 2nd, 2009

    Phil, how about generating a class for the links?
    For instance, if you have a controller in Controllers.HomeController, you could generate a class in the same namespace Controllers.HomeLinks or Controllers.HomeControllerLinks, which would be used like this:
    HomeLinks.About(122)

  12. Avatar for Andrew Davey
    Andrew Davey June 2nd, 2009

    What about web application projects? I thought build providers only work for web sites.
    IMO a view should not be responsible for creating URLs anyway!
    The controller should be deciding where user can go next (i.e. URLs) and passing the links to the view.
    Check out: http://www.assembla.com/wiki/show/snooze
    for a REST/URL centric approach to asp.net mvc.

  13. Avatar for Haacked
    Haacked June 2nd, 2009

    @Rob Build providers will work in Medium trust in ASP 4 AFAIK (but not in 3.5 SP1). I'm sure we can find a way to do this that does work in medium. This was just a proof-of-concept after all.
    @Andrew BuildProviders work in Web Applications too. The proof-of-concept David built used a WAP.

  14. Avatar for Zihotki
    Zihotki June 2nd, 2009

    This approach will help to increase performance. But what about refactoring? Due to the reason that these "magic action links" will be generated at build time we'll need to:
    1. rename method of action link
    2. run the site - "When the first request is made at runtime, ASP.NET tries to build the App_Code assembly"
    3. stop debugging
    4. fix all errors
    5. run again to verify all
    In my opinion this is very terrible. In my opinion it's better to use magic strings in this case and use a Resharper. When we trying to rename a method R# looks over the code for usages of this method and also it looks for strings equals to the name of the method and it ask to rename them too.
    And what about routes? Is the generator is smart enough to generate links based on routes? And what about unit tests?
    For me it's too much black magic at this moment with these generators. I prefer to store all these "magic strings" in some kind of Controllers and Views Managers and manage them manually.

  15. Avatar for Mike
    Mike June 2nd, 2009

    Seems like a lot of complexity just to generate a string. How about...
    public class AccountController : Controller
    {
    public const string NAME = "Account";
    public const string LOGIN_ACTION = "Login";
    public const string LOGOUT_ACTION = "Logout";
    ...
    }
    Then in your view,
    Html.ActionLink(AccountController.LOGIN_ACTION, AccountController.NAME);
    This approach is performant, type-safe, and easy to understand.

  16. Avatar for Zihotki
    Zihotki June 2nd, 2009

    PS. sorry for my pidgin English, it's a bit late here and I'm tired so I'm not able to catch all mistakes but I did my best.

  17. Avatar for Scott Allen
    Scott Allen June 2nd, 2009

    @Phil - I understand there are no extension properties, but since we already have a few helpers (Ajax, Html, Url, etc), it might be nice to have the provider generate a completely new class. RouteLink, perhaps. RouteLink.Home.About(123).
    Either way, I like it better than the ActionLink extension.

  18. Avatar for Haacked
    Haacked June 2nd, 2009

    @Mike You've only addressed the action name. What about action methods that have parameters? Wouldn't you like a type safe mechanism for specifying those paramters?

  19. Avatar for Peter Obiefuna
    Peter Obiefuna June 2nd, 2009

    @mike: Your suggestion may be simple but is actually just as good as embedding magic strings in views. There's no compile-time confirmation that those controller and action names are correct (renaming will not break compiles). And as Phil already noted, you still have to deal with args.

  20. Avatar for Vijay Santhanam
    Vijay Santhanam June 2nd, 2009

    I like where this is going!
    But how do you handle optional parameters?
    For example, I often have
    public ActionResult List(Paging paging) {return View(model);}
    where Paging class is custom bound. It's entirely optional and the model binder knows about default values.

  21. Avatar for Lanwin
    Lanwin June 2nd, 2009

    I like Link.Home().About(123)! The Enum version is to much verbose.

  22. Avatar for Nagarajan
    Nagarajan June 2nd, 2009

    Why can't use T4 for this?

  23. Avatar for Simone
    Simone June 2nd, 2009

    Using buildprovider will make the method "HomeAboutLink(123)" appear for real only at runtime. Refactoring tools will warn me of calls to non existing methods. What if I change the name of an Action? There will be no refactoring tool to help me change the ActionLink method name in the view.
    Or am I missing something obvious?

  24. Avatar for nic
    nic June 2nd, 2009

    We fought with those ActionLinks too, but since I wanted to keep the advantages of the generic ActionLink (like compile-time type checking, refactoring support) we created an overload that takes exactly one parameter.
    <%= Html.ActionLink((ProductController c) => c.Show, product, product.FullName) %>
    The ActionLink's signature is
    public string ActionLink<K, T>(Expression<Func<T, Func<K, ActionResult>>> action, K parameter, string linkText) where T : Controller
    Of course, this works only for ActionLinks that take one parameter, but since most of our ActionLinks do so, we have good performance with this. In cases where the ActionLink takes multiple parameters, we use the old school MvcFutures ActionLink
    <%= Html.ActionLink<HomeController>(c => c.Index()) %>

  25. Avatar for Elad Ossadon
    Elad Ossadon June 3rd, 2009

    OK, answering myself: That method I looked for is Html.BuildUrlFromExpression<T>()
    I would have expected it to be in UrlHelper and not in HtmlHelper, but at least it exists.

  26. Avatar for Eric hexter
    Eric hexter June 4th, 2009

    I pulled down the source code and implemented the UrlHelper version of this. It is an interesting approach, although I kind of struggle with why have a runtime generation versus a build time code gen.
    What is interesting.. is Resharp totally does not pick up the new methods created by the generation.. but Visual Studio understands the extensions and provides full intellisense with this approach.
    I like the idea of using this for Url generation particularly for my Form markup. Since I just need the url with out the parameters for forms. since all of my parameters typically come from my form input elements..... love to see more on this and perhaps a version that does this with a postBuild step similar to the aspCompile...
    keep up the experiements in alternative approaches, I love to see the little prototypes in areas of the framework that are not well documented.

  27. Avatar for Andy Kutruff
    Andy Kutruff June 4th, 2009

    I added expression caching to CLINQ that supports both closure expressions (expressions that capture values.) It yields a 100X increase for open expressions, and an 8X increase for closures. The code is well isolated from the rest of the framework, so you should be able to easily lift it out.
    For example, if you have
    Expression&lt;Func&lt;int, int&gt;&gt; myExpression = arg =&gt; arg + 1;
    myExpression.Compile()
    You just need to change it to:
    Expression&lt;Func&lt;int, int&gt;&gt; myExpression = arg =&gt; arg + 1;
    myExpression.CachedCompile()
    CLINQ 2.1
    It's in the ContinuousLinq.Expressions namespace / solution folder. Enjoy.

  28. Avatar for Haacked
    Haacked June 4th, 2009

    @Simone it works with Visual Studio Intellisense. Visual Studio knows to generate it. You can have compile time by simply pre-compiling your site.

  29. Avatar for Simone
    Simone June 5th, 2009

    Thanks... is it accomplished by the pageparser filter?

  30. Avatar for Andrew Davey
    Andrew Davey June 5th, 2009

    I'm using a web application project.
    How can the build provider help if I want to generate Urls in my action method (not in the views)? For example, redirecting to another action...

  31. Avatar for Andrew Davey
    Andrew Davey June 5th, 2009

    Maybe creating a custom VS tool (by implementating IVsSingleFileGenerator) would be a more robust approach.

  32. Avatar for sulumits retsambew
    sulumits retsambew June 12th, 2009

    nice tutorial for newbie like me,, good article and keep posting thank you

  33. Avatar for Jamie Cansdale
    Jamie Cansdale June 15th, 2009

    Another solution would be to constrain the complexity of expressions that are allowed as parameters. Simple expressions can be evaluated quickly using a little reflection.
    For example, the following expression evaluator would support constants and for loops (your example):
    static object GetValue(Expression expression)
    {
    ConstantExpression constant = expression as ConstantExpression;
    if (constant != null)
    {
    return constant.Value;
    }
    MemberExpression member = expression as MemberExpression;
    if (member != null)
    {
    FieldInfo field = member.Member as FieldInfo;
    ConstantExpression c = member.Expression as ConstantExpression;
    if (field != null && c != null)
    {
    return field.GetValue(c.Value);
    }
    }
    throw new ArgumentException("ComplexExpressionsNotSupported", "expression");
    }
    With a little more work I suspect you could nail the common cases. It is really necessary to support all complex expressions as ActionLink parameters? I think not supporting them would be a reasonable compromise. :)