Mark Gilbert's Blog

Science and technology, served light and fluffy.

I just can’t decide – Missing Parameters and MVC Routing

Recently, I started seeing ELMAHs for one of my projects that suggested it was being called without a required ID in the request.  After playing with it for a bit, I found that that was indeed the case.  Valid requests look like this:

http://MySite.com/Home.aspx/Index/42

But I was getting requests that looked like this:

http://MySite.com/Home.aspx/Index

Up to this point my route for the valid requests had been this:

routes.MapRoute( _
    "DefaultWithSongID", _
    "home.aspx/index/{SongID}", _
    New With {.controller = "Home", .action = "Index", .SongID = UrlParameter.Optional} _
)

And my Index controller action looked like this:

Function Index(ByVal SongID As Long) As ActionResult
    ...
End Function

My first attempt was to define a second route – one that left the SongID out, but would still be handled by my controller action:

routes.MapRoute( _
    "DefaultNoSongID", _
    "home.aspx/index", _
    New With {.controller = "Home", .action = "Index", .SongID = 0} _
)

Had this worked, my next step was to handle the “0” case in the action, but I never made it that far.  When I fired up the solution in the debugger, I was greeted with this:

The current request for action ‘Index’ on controller type ‘HomeController’ is ambiguous between the following action methods:

System.Web.Mvc.ActionResult Index() on type RoutingSample.HomeController

System.Web.Mvc.ActionResult Index(Int64) on type RoutingSample.HomeController

Hmm.  Ok.  What if I left the .SongID parameter out of the declaration altogether?

routes.MapRoute( _
    "DefaultNoSongID", _
    "home.aspx/index", _
    New With {.controller = "Home", .action = "Index"} _
)

Nope.  No dice.  Same error.  So, back to the drawing board.

I reasoned that the error was due to the multiple routes in the routing table that referenced the Index action of the Home controller, so the solution seemed to require that I handle both cases  – with and without SongID on the URL – in a single route.  Here’s what I came up with for that revised route:

routes.MapRoute( _
    "DefaultWithSongID", _
    "home.aspx/index/{SongID}", _
    New With {.controller = "Home", .action = "Index", .SongID = UrlParameter.Optional} _
)

Why Mark, this looks suspiciously like the original route.  Why yes, my dear reader, it DOES look like the original one, doesn’t it.  Ahem.

With SongID back to being defined as “optional”, I went back to just a single Index method, and tried to simply handle the “missing SongID” case:

    Function Index(ByVal SongID As Long) As ActionResult
        If (SongID = 0) Then Return View("index", "site", "No Song ID used")
        Return View("index", "site", String.Format("Song ID {0} used", SongID.ToString))
    End Function

Unfortunately, that didn’t work either.  I got this error when the page was requested:

The parameters dictionary contains a null entry for parameter ‘SongID’ of non-nullable type ‘System.Int64’ for method ‘System.Web.Mvc.ActionResult Index(Int64)’ in ‘RoutingSample.HomeController’. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.

Parameter name: parameters

This was, I believe, the error that had me troubleshooting this to begin with.  Awesome – I‘m now coding in circles.

Ok, let’s think about this.  Normally, assigning “Nothing” to a variable of type Long results in it being set to 0.  MVC, though, was apparently having problems doing that implicit cast, so I tried modifying my controller action to take a nullable Long:

    Function Index(ByVal SongID As Long?) As ActionResult
        If (IsNothing(SongID)) Then Return View("index", "site", "No Song ID used")
        Return View("index", "site", String.Format("Song ID {0} used", SongID.ToString))
    End Function

As you can see, I also had to modify the check from “SongID = 0” to “IsNothing(SongID)”.  Those two changes allowed it to work.  I could now request Home.aspx/Index and Home.aspx/Index/42, and both would be handled correctly without errors.

Now, checking for the missing SongID in the controller action isn’t a horrible solution, but what would have been a little nicer is to be able to define a route where “SongID” was not present at all, and be able to map that route to a controller action – all from the Global.asax.  After I put the above solution into place, I did some additional digging on the internets to see if I was truly missing something.  What I found was this post by Robert Koritnik, where he explains why this scenario occurs, and how to write a custom “action method selector attribute”: http://erraticdev.blogspot.com/2011/02/improving-aspnet-mvc-maintainability.html.  This attribute basically acts as a hint for MVC to find the correct action method based on the parameters present in the request.

I liked this solution and Koritnik explains with much more clarity than I have why the nullable approach is a less than desirable solution.  However, by this point I had run out of time to implement anything else, so my “nullable” approach would have to stand.

Sigh.  I hate it when reality gets in the way of perfection.

Advertisements

July 1, 2011 - Posted by | ASP.NET MVC

Sorry, the comment form is closed at this time.

%d bloggers like this: