6

I know that if you think you found a bug in the .NET framework you are most likely wrong, but that's why I'm writing this question, so please hear me out.

I am fairly certain that there is a difference between the routes in .NET 3.5 and .NET 4.0 when it comes to optional parameters. Specifically if you have more than one optional parameter in your route. I have not been able to find this breaking change noted in any release notes of either .NET 4.0 or MVC 3 so thats why I call it a bug.

EDIT: This bug only manifests itself when you try to build a route url using code like the url or html helpers in mvc. If you actually request the url in a browser, in an actual mvc app, it works just fine. So if my test app below were an actual mvc app there wouldn't be a problem if you tried to request '/root/test1'.

What I need you to do is run the following test program, it should be fairly self-explanatory, but basically it just sets up a route with some optional paramters.

  1. Create a new .NET 4 console application

  2. Change the target framework to '.NET Framework 4' instead of '.NET Framework 4 Client Profile'

  3. Add references to:
    System.Web 4.0
    System.Web.Routing 4.0
    System.Web.Mvc 3.0

  4. Paste following code to the program.cs file, overwriting any previous content:

    using System;
    using System.IO;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    
    public class Program
    {
        static void Main()
        {
            var httpCtx = new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://localhost/", null), new HttpResponse(new StringWriter())));
    
            var routes = RouteTable.Routes;
            routes.MapRoute("Test", "root/{test1}/{test2}/{test3}", new { test2 = UrlParameter.Optional, test3 = UrlParameter.Optional });
    
            var context = new RequestContext(httpCtx , new RouteData());
    
            var url = new UrlHelper(context);
    
            var expected1 = "/root/test1";
            var expected2 = "/root/test1/test2";
            var expected3 = "/root/test1/test2/test3";
    
            var actual1 = url.RouteUrl("Test", new { test1 = "test1" });
            var actual2 = url.RouteUrl("Test", new { test1 = "test1", test2 = "test2" });
            var actual3 = url.RouteUrl("Test", new { test1 = "test1", test2 = "test2", test3 = "test3" });
    
            var result1 = actual1 == expected1;
            var result2 = actual2 == expected2;
            var result3 = actual3 == expected3;
    
            Console.WriteLine("Test 1: {0} ({1})", result1 ? "Success" : "Fail", result1 ? string.Format("'{0}'", actual1) : string.Format("Expected '{0}' but was '{1}'", expected1, actual1 ?? "<null>"));
            Console.WriteLine("Test 2: {0} ({1})", result2 ? "Success" : "Fail", result2 ? string.Format("'{0}'", actual2) : string.Format("Expected '{0}' but was '{1}'", expected2, actual2 ?? "<null>"));
            Console.WriteLine("Test 3: {0} ({1})", result3 ? "Success" : "Fail", result3 ? string.Format("'{0}'", actual3) : string.Format("Expected '{0}' but was '{1}'", expected3, actual3 ?? "<null>" ));
            Console.ReadLine();
        }
    }
    
  5. Run program, and note the resulting urls.
    On my machine the result is:

    Test 1: Fail    (expected '/root/test1' but was '<null>')
    Test 2: Success ('/root/test1/test2')
    Test 3: Success ('/root/test1/test2/test3')
    
  6. Now change the target framework to .NET 3.5 add references to:
    System.Web.Mvc 2.0
    System.Web.Routing 3.5
    System.Web.Abstrations 3.5

  7. Run the program again and see the 3 test cases all succeed.
    This time the result is:

    Test 1: Success ('/root/test1')  
    Test 2: Success ('/root/test1/test2')  
    Test 3: Success ('/root/test1/test2/test3')
    

So it seems to me there is a bug in the .NET 4 or MVC 3. If you find the same issue please vote on the following issue in connect: https://connect.microsoft.com/VisualStudio/feedback/details/630568/url-routing-with-two-optional-parameters-unspecified-fails-on-asp-net-mvc3-rc2#details

Feel free to test this out in a regular MVC application if you think theres something wrong in the test program.

JohannesH
  • 6,430
  • 5
  • 37
  • 71

2 Answers2

4

So, Phil Haack just released a blog post detailing that this is a known issue and will be fixed in the next release of the .NET framework, or maybe if we are lucky as a bug fix to the particular dll. I'm not exactly sure how they will fix it though. Whether they will outlaw more than 1 optional parameter, or whether they will resolve it in the manner I've outlined in the comments to Darin's answer. At least they know about it and can take steps to specify the expected behavior with more than one optional parameter more in the future.

JohannesH
  • 6,430
  • 5
  • 37
  • 71
2

Only the last parameter of a route definition can be optional and this rule has been enforced in ASP.NET MVC 3. In your example you have both test2 and test3 optional which is not possible. Consider the following URL:

root/test1/test2

Given this URL the routing engine cannot say whether test2="test2" and test3="" or test2="" and test3="test2". So why generating an URL which can never be parsed back to it's constituent tokens?

Now maybe that's a bug or a feature or call it whatever but in all cases you should avoid having such routes. IMHO the bug here is that the framework doesn't throw an exception telling you that only the last parameter in a route definition can be optional, but of course that's just my opinion.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Hi Darin, thanks for the answer. In your example MVC2 would set test2="test2" which is the intuitive way to resolve the issue, and which allows several optional parameters. I don't see why it would be a problem specifying this as the accepted resolution as long as it is clearly documented. As you say its hard to know whether its a bug or not. The only thing I can say for certain is that there is an undocumented (breaking) difference between MVC2 and MVC3. – JohannesH Feb 01 '11 at 17:16
  • Also, the routing engine in MVC3 has no problem routing a specific url correctly... Its the url generation that fails to work. – JohannesH Feb 01 '11 at 17:38
  • @JohannesH, the thing that I agree with you is that there is an undocumented change. But leaving that apart when you are talking about an intuitive way to resolve the issue I don't agree with you: for me both are equally possible. Why that would be the intuitive way if `test2` can be empty and we wanted to give a value to `test3`? How would you do that? It's just a case you cannot have if both were optional: `test2` empty and `test3` has a value assuming this url is requested: `root/test1/test2`. – Darin Dimitrov Feb 01 '11 at 17:47
  • 1
    For me the intuitive rule would be something like: "Optional parameters must be specified in the order they are used.". I agree in the case where you specify test3 but not test2, some exception should be thrown. In MVC2 the result of calling Url.Action with only the test2 parameter value set to "test2" results in the url: "/root/test1/test2", I like that. However calling Url.Action with just a test3 parameter value, set to "test3", results in the url: "/root/test1/System.Web.Mvc.UrlParameter/test3", as I said, in this case I would rather have it throw an exception. – JohannesH Feb 01 '11 at 19:44
  • 1
    I believe @Darin Dimitrov is wrong about UrlParameter.Optional misuse. It should be a bug. Documentation says nothing about only one last optional parameter in a route definition. At the same time C# 4.0 allows many optional arguments for methods - http://msdn.microsoft.com/en-us/library/dd264739.aspx – Alexander Prokofyev Feb 17 '11 at 06:40
  • I have found what using string.Empty instead of UrlParameter.Optional also has this behavior. So url masks like "Archive/{year}/{month}" with defaults **new { controller = "Article", action = "GetArchive", year = "", month = "" }** are now outlaws. – Alexander Prokofyev Feb 17 '11 at 09:44