Freigeben über


C# method overload resolution can play tricks on you

While working on T4MVC, I ran into a very tricky C# compiler behavior that I thought I’d share.  T4MVC creates various overloads that allow you to avoid literal strings.  One such case is that you can replace:

 routes.MapRoute(
    "UpcomingDinners", 
    "Dinners/Page/{page}", 
    new { controller = "Dinners", action = "Index" }
);

with

 routes.MapRoute(
    "UpcomingDinners",
    "Dinners/Page/{page}",
    MVC.Dinners.Index()
);

This works because T4MVC adds its own MapRoute overload extension method (which it puts in the global namespace):

 public static Route MapRoute(this RouteCollection routes, string name, string url, ActionResult result);

And of course I had tested this out before publishing the template, and it worked perfectly well with NerdDinner, which I’ve been using as my little test app.

But then, a developer sends me an email telling me that while the template is generally working well for him, this one scenario is not.  Instead of it calling the T4MVC overload, it binds to the (less specific) overload in ASP.NET MVC (coming from the System.Web.Mvc namespace):

 public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults);

This was very puzzling, since the call was passing an ActionResult, and the more specific overload should ‘win’.  Eventually, we discovered that the only difference between my working app and his non-working app is that he was putting his ‘using’ statements inside the namespace, while I was putting them outside!

e.g. I had:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace NerdDinner {

    public class MvcApplication : System.Web.HttpApplication {

While he had:

 namespace NerdDinner {
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;

    public class MvcApplication : System.Web.HttpApplication {

Note that both ways are perfectly valid, and if you look at C# samples out there, you’ll find a mix of both.  It pretty much comes down to personal preference, and you would generally expect them to behave the same way.  So why the heck was that causing the C# binding behavior to change?!

It turned out that the root of the issue was that T4MVC was generating its overload in the global namespace (i.e. not in a namespace).  So it had to do with the way C# searched namespaces (see C# spec for details), causing it to find the System.Web.Mvc overload before it even considers the global namespace when the using statements are inside.  While when they’re outside, System.Web.Mvc and the global namespace are looked at together, letting the more specific overload take over.  Tricky stuff!

So what’s the moral from this story?  There are a couple:

Putting things in the global namespace is evil.  I was guilty of doing it in T4MVC.  I guess I had figured that being generated code, we just wanted it to be available everywhere without forcing the user to deal with namespaces, but it was a mistake.  I have since moved away from that, instead adding things to the System.Web.Mvc namespace.  This may be slightly questionable as well, since it’s adding to a framework namespace it doesn’t own, but given the nature of them T4 template, it’s acceptable.  Now it works equally well wherever the ‘usings’ are.

Overloads that take ‘object’ are pretty evil.   ASP.NET MVC makes common use of the anonymous object pattern to pass default values.  This is why their overload is typed as object, and lets you pass new { controller = "Dinners", action = "Index" } .  But while this is convenient, it really increases the odds of confusing the resolution rules, so I’m not a huge fan of them.  Some people will go as far as saying the method overloads are generally evil, but I think they are very useful when not abused.

Comments

  • Anonymous
    July 31, 2009
    All I can say is a cool bit of detective work there. Steve :D

  • Anonymous
    August 14, 2009
    Cool template. Keep it up. I am using it and always stay up to date. After MVC2Preview1 is out, I would like to use T4MVC so I have a question if you have any plan yet to support Areas in MVC2Previw1. :)

  • Anonymous
    August 14, 2009
    Forget to tell you. There is one problem. T4MVC doesn't include namespace of Custom ActionResult so I have to always include it myself in T4MVC.tt

  • Anonymous
    August 20, 2009
    The comment has been removed

  • Anonymous
    September 02, 2009
    @ensecoz: I haven't had a chance to add support for new MVC2Preview1 features, but I will look into that. Also, I just fixed the namespace issue you reproted in 2.4.02.