Routing Issue in ASP.NET MVC 3 RC 2

Update:  This issue is also present in ASP.NET MVC 3 RTM.

 

     Introduction:

 

          Two weeks ago, ASP.NET MVC team shipped the ASP.NET MVC 3 RC 2 release. This release includes some new features and some performance optimization. This release also fixes most of the bugs but still some minor issues are present in this release. Some of these issues are already discussed by Scott Guthrie at Update on ASP.NET MVC 3 RC2 (and a workaround for a bug in it). In addition to these issues, I have found another issue in this release regarding routing. In this article, I will show you the issue regarding routing and a simple workaround for this issue.

 

    Description:

 

          The easiest way to understand an issue is to reproduce it in the application. So create a MVC 2 application and a MVC 3 RC 2 application. Then in both applications, just open global.asax file and update the default route as below,

 

 

            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id1}/{id2}", // URL with parameters
                new { controller = "Home", action = "Index", id1 = UrlParameter.Optional, id2 = UrlParameter.Optional } // Parameter defaults
            );

 

 

          Then just open Index View and add the following lines,

 

 

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 

Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <% Html.RenderAction("About"); %>
</asp:Content>

 

 

         The above view will issue a child request to About action method. Now run both applications. ASP.NET MVC 2 application will run just fine. But ASP.NET MVC 3 RC 2 application will throw an exception as shown below,

 

 

 

 

         You may think that this is a routing issue but this is not the case here as both ASP.NET MVC 2 and ASP.NET MVC  3 RC 2 applications(created above) are built with .NET Framework 4.0 and both will use the same routing defined in System.Web. Something is wrong in ASP.NET MVC 3 RC 2. So after digging into ASP.NET MVC source code, I have found that the UrlParameter class in ASP.NET MVC 3 RC 2 overrides the ToString method which simply return an empty string.

 

 

    public sealed class UrlParameter
    {
        public static readonly UrlParameter Optional = new UrlParameter();

        private UrlParameter()
        {
        }

        public override string ToString()
        {
            return string.Empty;
        }
    }

 

 

         In MVC 2 the ToString method was not overridden. So to quickly fix the above problem just replace UrlParameter.Optional default value with a different value other than null or empty(for example, a single white space) or replace UrlParameter.Optional default value with a new class object containing the same code as UrlParameter class have except the ToString method is not overridden (or with a overridden ToString method that return a string value other than null or empty). But by doing this you will loose the benefit of ASP.NET MVC 2 Optional URL Parameters. There may be many different ways to fix the above problem and not loose the benefit of optional parameters. Here I will create a new class MyUrlParameter with the same code as UrlParameter class have except the ToString method is not overridden. Then I will create a base controller class which contains a constructor to remove all MyUrlParameter route data parameters, same like ASP.NET MVC doing with UrlParameter route data parameters early in the request.

 

 

    public class BaseController : Controller
    {
        public BaseController()
        {
            if (System.Web.HttpContext.Current.CurrentHandler is MvcHandler)
            {
                RouteValueDictionary rvd = ((MvcHandler)System.Web.HttpContext.Current.CurrentHandler).RequestContext.RouteData.Values;
                string[] matchingKeys = (from entry in rvd
                                         where entry.Value == MyUrlParameter.Optional
                                         select entry.Key).ToArray();
                foreach (string key in matchingKeys)
                {
                    rvd.Remove(key);
                }
            }
        }
    }

    public class HomeController : BaseController
    {
        public ActionResult Index(string id1)
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";
            return View();
        }

        public ActionResult About()
        {
            return Content("Child Request Contents");
        }
    }

 

 

    public sealed class MyUrlParameter 
    {
        public static readonly MyUrlParameter Optional = new MyUrlParameter();

        private MyUrlParameter()
        {
        }
    }

 

 

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
    	"Default", // Route name
    	"{controller}/{action}/{id1}/{id2}", // URL with parameters
    	new { controller = "Home", action = "Index", id1 = MyUrlParameter.Optional, id2 = MyUrlParameter.Optional } // Parameter defaults
    );

 

 

         MyUrlParameter class is a copy of UrlParameter class except that MyUrlParameter class not overrides the ToString method. Note that the default route is modified to use MyUrlParameter.Optional instead of UrlParameter.Optional. Also note that BaseController class constructor is removing MyUrlParameter parameters from the current request route data so that the model binder will not bind these parameters with action method parameters. Now just run the ASP.NET MVC 3 RC 2 application again, you will find that it runs just fine. 

 

         In case if you are curious to know that why ASP.NET MVC 3 RC 2 application throws an exception if UrlParameter class contains a ToString method which returns an empty string, then you need to know something about a feature of routing for url generation. During url generation, routing will call the ParsedRoute.Bind method internally. This method includes a logic to match the route and build the url. During building the url, ParsedRoute.Bind method will call the ToString method of the route values(in our case this will call the UrlParameter.ToString method) and then append the returned value into url. This method includes a logic after appending the returned value into url that if two continuous returned values are empty then don't match the current route otherwise an incorrect url will be generated. Here is the snippet from ParsedRoute.Bind method which will prove this statement.  

 

 

    if ((builder2.Length > 0) && (builder2[builder2.Length - 1] == '/'))
    {
        return null;
    }
    builder2.Append("/");
    ...........................................................
    ...........................................................
    ...........................................................
    ...........................................................
    if (RoutePartsEqual(obj3, obj4))
    {
	builder2.Append(UrlEncode(Convert.ToString(obj3, CultureInfo.InvariantCulture)));
	continue;
    }

 

 

         In the above example, both id1 and id2 parameters default values are set to UrlParameter object and UrlParameter class include a ToString method that returns an empty string. That's why this route will not matched.   

 

 

    Summary:

 

          In this article I showed you the issue regarding routing and also showed you how to workaround this problem. I explained this issue with an example by creating a ASP.NET MVC 2 and a ASP.NET MVC 3 RC 2 application. Finally I also explained the reason for this issue. Hopefully you will enjoy this article too.

 

4 Comments

Comments have been disabled for this content.