RSS

Tag Archives: Routing

.NET 4 URL Routing for Web Services (asmx)

This post describes how to perform Url Routing to a Web Service. If you’re just interested in the solution, click here.

OK, this was a tough one.

The goal was to implement URL routing for an existing web site that has asmx Web Services. The intention was to allow white-labeling of the Web Services. You might argue why one would want to do that, so I’ll summarize by writing that one of the reasons to do so was that I wanted to enjoy URL Routing advantages, without having to rewrite the existing Web Services into a more “modern” alternative such as MVC. Other reasons are obvious: Web APIs are becoming more and more URL friendly and allow great flexibility.

Note that the web site is written in .NET 4 over IIS7, so I won’t get into how to configure routing or web services. You can read here about Routing, and specifically about Routing in WebForms here.

Unlike routing in MVC which you can’t really do without, in WebForms this is less trivial. In WebForm’s Routing we have the MapPageRoute method, but we have nothing specific for Web Services or other handlers. I already blogged about how to route to a handler (ashx), but I found it much less trivial to accomplish routing for Web Services, and in this post I’ll try to explain why.

Setup
The Web Service in this example has a simple HelloWorld method that returns a greeting:

using System.Web;
using System.Web.Services;

[WebService(Namespace = "http://evolpin/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class MyWebService : System.Web.Services.WebService
{
    [WebMethod]
    public string HelloWorld(string name)
    {
        string product = HttpContext.Current.Request.RequestContext.RouteData.Values["product"] as string;
        return string.Format("Hello {0} from {1}", name, product);
    }
}
  • Line 6: very important – allow the Web Service to be invoked from a client script.
  • Line 12 I attempt to retrieve the {product} part from the Route data. This is an example for the desired white-labeling.

The Global.asax looks like this:

<%@ Application Language="C#" %>
<%@ Import Namespace="System.Web.Routing" %>
<script runat="server">
    void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes(RouteTable.Routes);
    }

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.Add(new System.Web.Routing.Route("{product}/xxx/{*pathInfo}", new WebServiceRouteHandler("~/MyWebService.asmx")));

        routes.MapPageRoute("", "{*catchall}", "~/Test.aspx");
    }
</script>

The web.config looks like this:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <webServices>
      <protocols>
        <add name="HttpGet"/>
        <add name="HttpPost"/>
      </protocols>
    </webServices>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
</configuration>

The test page (Test.aspx) looks like this:
test page
And its code:

<%@ Page Language="C#" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.3.min.js"></script>
</head>
<body>
    <form id="form1" runat="server">
    <script type="text/javascript">
        function doGet() {
            $.ajax({
                url: "http://localhost/Routing/" + $('#product').val() + "/xxx/HelloWorld?name=evolpin",
                type: "GET",
                success: function (result) {
                    alert(result.firstChild.textContent);
                }
            });
        }

        function doPostXml() {
            $.ajax({
                url: "http://localhost/Routing/" + $('#product').val() + "/xxx/HelloWorld",
                type: "POST",
                data: { name: 'evolpin' },
                success: function (result) {
                    alert(result.firstChild.textContent);
                }
            });
        }

        function doPostJson() {
            $.ajax({
                url: "http://localhost/Routing/" + $('#product').val() + "/xxx/HelloWorld",
                type: "POST",
                contentType: 'application/json',
                data: JSON.stringify({ name: 'evolpin' }),
                success: function (result) {
                    alert(result.d);
                }
            });
        }
    </script>
    <div>
        <input type="text" id='product' value='MyProduct' />
        <input type="button" value='GET' onclick='doGet();' />
        <input type="button" value='POST xml' onclick='doPostXml();' />
        <input type="button" value='POST json' onclick='doPostJson();' />
    </div>
    </form>
</body>
</html>
  • Lines 11-19 perform a GET with a query string.
  • Lines 21-30 perform a POST with regular text/xml content type.
  • Lines 32-42 perform a POST using JSON.

Note that all these JavaScript methods take the “product name” from the ‘product’ text box.

And finally, the C# client test code which will invoke the Web Service using SOAP looks like this:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (localhost.MyWebService ws = new localhost.MyWebService())
            {
                ws.Url = "http://localhost/Routing/MyProduct/xxx";
                string s = ws.HelloWorld("evolpin");
            }
        }
    }
}

Line 9: Changing the target Url to the Route.

The code
After googling quite a lot (it seems like there’s very little information about this), I saw several posts that used the following solution:

using System.Web;
using System.Web.Routing;
public class WebServiceRouteHandler : IRouteHandler
{
    private string virtualPath;

    public WebServiceRouteHandler(string virtualPath)
    {
        this.virtualPath = virtualPath;
    }

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new System.Web.Services.Protocols.WebServiceHandlerFactory().GetHandler(HttpContext.Current, "*", this.virtualPath, HttpContext.Current.Server.MapPath(this.virtualPath));
    }
}

While this solution works for SOAP, if you try to GET or POST, the route won’t work well and you’ll end up receiving the auto generated WSDL documentation instead of calling your actual method. This occurs because the route that I was using was an extensionless route and for reasons explained below, the factory was unable to establish which protocol to use, so it ended-up with the Documentation handler. The WebServiceHandlerFactory attempts to understand which handler should be used to serve the request, and to do so it queries the different protocols it supports (e.g. SOAP, GET, POST etc.). I’ll use a GET request in order to explain why this doesn’t work well. The GET request used in this example is:

http://localhost/Routing/MyProduct/xxx/HelloWorld?name=evolpin
  • ‘xxx’ is the Web Service alternate route name.
  • ‘MyProduct’ is the sample product using the white-label feature.
  • ‘HelloWorld’ is the web method name that I would like to invoke.
  • ‘name’ is the name of the argument.

WebServiceHandlerFactory

The above image shows how the factory iterates over the available server protocols in an attempt to locate one suitable to serve the request. Here are the different supported protocols:

ServerProtocolFactories

If we peak into one of the supported protocols, GET, we can see that it queries the PathInfo. If the PathInfo is too short, a null is returned and the factory will continue to search for a suitable protocol.

HttpGetServerProtocolFactory

The PathInfo in this sample GET request is empty (“”). This is because I’m using an extensionless route. To make a long story short, after drilling down into the internals, the PathInfo in IIS7 is built on the understanding what the actual path info is (e.g. /HelloWorld). Because of the extensionless URL, the IIS7 fails to determine what the PathInfo is and sets it to empty, as seen below:

IIS7WorkerRequest

Now that I realized that there’s a problem with extensionless URLs, as IIS can’t parse the PathInfo correctly, I tried changing the route by adding ‘.asmx’ to it, hoping that this will help to resolve the correct path info. So I changed Global.asax and Test.aspx to use the .asmx, and the new route looks like this:

http://localhost/Routing/MyProduct/xxx.asmx/HelloWorld

When I tried GET or POST (using xml for the response content type), the new route worked! IIS7WorkerRequest was able to parse the PathInfo and the GET/POST protocol classes “were content” and used successfully. However, when I tried to use JSON I got back the following error:

System.InvalidOperationException: Request format is invalid: application/json; charset=UTF-8.
   at System.Web.Services.Protocols.HttpServerProtocol.ReadParameters()
   at System.Web.Services.Protocols.WebServiceHandler.CoreProcessRequest()

Looking at HttpServerProtocol.ReadParameters, I could see that there’s an iteration which attempts to determine which reader class should be used, according not only to the request type (e.g. POST), but also according to the content type:

HttpServerProtocolReadParameters

So I placed a breakpoint in the Route handler and drilled down to see what readerTypes exist and found out that there were two: UrlParameterReader and HtmlFormParameterReader:

ref1

As the debugger showed that hasInputPayload was ‘true’, I realized that the HtmlFormParameterReader was the one used. Looking at the code there, it became clear why using JSON failed:

HtmlFormParameterReader

I realized that unless I am missing something, WebServiceHandlerFactory was simply not good enough because while it did solve the GET/POST and xml content-type routing issue, it wasn’t able to handle the JSON content, which I wasn’t willing to give up on.

I tried looking for a different approach, placed a breakpoint in the HelloWorld method and invoked it “old fashion way”, without any routing or JSON. This is what I saw in the debugger when I was searching for which handler was used:

ScriptHandlerFactory1

What’s this? It seems like the built-in ASP.NET handlers are not using the WebServiceHandlerFactory directly, but a certain ScriptHandlerFactory. OK, so let’s review the debugger when a JSON request is sent:

ScriptHandlerFactory2

It seems like for JSON, the “built-in mechanism” is not using the WebServiceHandlerFactory at all, but a RestHandlerFactory, wrapped by the ScriptHandlerFactory. I have been using the wrong factory all along! Here’s the GetHandler of ScriptHandlerFactory:

ScriptHandlerFactory

As you can see, the ScriptHandlerFactory is a wrapper that instantiates other handlers and wrappers according to the request method and content types. Using the ScriptHandlerFactory seems like the correct option in order to have routing invoke the web service correctly, with different request methods and content types.

Solution:
Unfortunately, for some reason ScriptHandlerFactory is internal and non-public. So I had to use Reflection to invoke it. However, this didn’t work well because I kept receiving wrong handler classes (especially when I removed the .asmx added before to the route). After doing some more Reflection research, and remembering that the logic for retrieving the handlers and protocols was very dependent on the PathInfo, I was looking for a way to change it. PathInfo is actually a getter-only property which is dependent how IIS resolves the path. Fortunately, it seems like the path can be changed by calling the HttpContext.RewritePath method. So the resulting code looks like this:

using System;
using System.Web;
using System.Web.Routing;
public class WebServiceRouteHandler : IRouteHandler
{
    private static IHttpHandlerFactory ScriptHandlerFactory;
    static WebServiceRouteHandler()
    {
        var assembly = typeof(System.Web.Script.Services.ScriptMethodAttribute).Assembly;
        var type = assembly.GetType("System.Web.Script.Services.ScriptHandlerFactory");
        ScriptHandlerFactory = (IHttpHandlerFactory)Activator.CreateInstance(type, true);
    }

    private string virtualPath;
    public WebServiceRouteHandler(string virtualPath)
    {
        this.virtualPath = virtualPath;
    }

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        string pathInfo = requestContext.RouteData.Values["pathInfo"] as string;
        if (!string.IsNullOrWhiteSpace(pathInfo))
            pathInfo = string.Format("/{0}", pathInfo);

        requestContext.HttpContext.RewritePath(this.virtualPath, pathInfo, requestContext.HttpContext.Request.QueryString.ToString());
        var handler = ScriptHandlerFactory.GetHandler(HttpContext.Current, requestContext.HttpContext.Request.HttpMethod, this.virtualPath, requestContext.HttpContext.Server.MapPath(this.virtualPath));
        return handler;
    }
}
  • Lines 6-12 create a static ScriptHandlerFactory (no need to recreate it every time that the RouteHandler is used.
  • Lines 14-18 is a constructor that receives the virtual path for which the RouteHandler is instantiated for.
  • Lines 22-24: determine the pathInfo we want.
  • Line 26 modifies the PathInfo. This is the magic!
  • Line 27 invokes the logic of the ScriptHandlerFactory, which returns the appropriate handler according to the requset method and content type.

And the result (GET/POST, xml/JSON):

test page 2

SOAP (this was tested working with Soap 1.1 and 1.2):

soap1

Summary
I would be more than happy if someone has a better solution to this. The described solution here is still not complete. Besides the used Reflection, what’s missing is that the Documentation protocol (i.e. WSDL auto generation) should invoke the different methods using the {product} of the Url Route. If you try to use this solution and activate the HelloWorld method using the WSDL help page,  you’ll see that it uses the wrong url for invocation.

What’s good about this solution is that it’s very simple, it allows SOAP, GET, POST and JSON. It also allows you to use Url routing advantages, and without having to use the asmx extension in the route. In other words, complete Url Routing flexibility. I also successfully tested a version of this solution in IIS6.

Credits: I used the excellent ILSpy for Reflection, which is a very good substitute to Red-Gate’s not-free-any-more Reflector.

 
9 Comments

Posted by on 30/12/2012 in Software Development

 

Tags: ,

ASP.NET WebForms Routing

This post is more of a “technical completion” post for using Routing in WebForms, which covers some of the issues I have come across. If you require a quick start on Routing, or technical MVC stuff, please read this post first.

I was happy to learn that it’s quite easy to use MS’ built-in Url Routing solution for ASP.NET WebForms. This is quite easy to accomplish in IIS 7, and requires some tweaking in IIS 6. Read this on how to configure your web.config file to support routing according to your IIS configuration. You may want to check that Url Routing is installed.

One pit fall that I’ve encountered was when I needed to deploy a routing solution on a certain server. There seemed to be a slight configuration difference from my development machine, and routing did not work. What I eventually did was to perform a minor tweak in the configuration file, and added a runAllManagedModulesForAllRequests as described here. Note that there are those who consider this configuration tweak “inappropriate” as far as performance, resources and error prone, but their suggested solutions didn’t seem to solve my issue. I decided to perform a simple check, what ASP.NET MVC’s template is configured like – and runAllManagedModulesForAllRequests=true is exactly that. I reckon that if the guys at MS provide this configuration in ASP.NET MVC, it’s good enough for WebForms too.

Enough with the configuration, where are those pitfalls?

As with ASP.NET MVC, the routes are set in the Global.asax file. Here are some routes that will be used in this post:

get.aspx is the sample page in this post. It contains a single PageMethod and will be our test case:

PageMethods
Unlike Web Services (asmx) WebMethods, there is an issue with Url Routing when it comes to PageMethods. It seems like the client is unable to provide the correct url for the page, and the server seems to fail in figuring out which page and method should be invoked. The error received in the client is “405 Method Not Allowed“, and the response contains a somewhat confusing “The HTTP verb POST used to access path ‘…’ is not allowed.” message, which basically means that the Url isn’t resolved correctly and therefore does not reach the correct PageMethod.

Indeed, the Url the client sends by default to our get.aspx page is: “http://localhost:54885/WebSite2/get/1/Get&#8221;.

  • The first “get” is the route which routes to get.aspx;
  • The “1” is a simple {id}.
  • The second “Get” is the PageMethod.

Because we received the 405 error message, I used the Route Debugger, and as expected, the Route Debugger showed a “No Match” on that url. Initially I thought that it would be easy to write a RouteHandler and provide a generic solution for PageMethod urls, but after I started coding I noticed that it’s not as easy as it may seem. Possible, but not as easy as the workaround. In order to workaround, all you have to do is to set the path in the client JavaScript to the correct page and you’re done. This is done like so:

Note: You can read here how to call your PageMethods using jQuery.

HttpHandlers – ashx files

It seems like ASP.NET 4 has no support for routing to ashx handler files. When you attempt to route to an ashx you’ll get the following exception:

Type ‘MyHandler’ does not inherit from ‘System.Web.UI.Page’.

Fortunately, this is quite easy to get by, because coding a Route handler which will take care of ashx files is trivial enough. Note, that the solution suggested here is based on a Web site and not a Web application, so the route handler code resides in App_Code.

So, there are three steps to this:

  1. Refactor your ashx code to an App_Code class which inherits from IHttpHandler.
  2. Write a simple Route handler which will create that handler when requested to.
  3. Add the new route handler to the routes defined in Global.asax.

Step 1: Refactor the handler – basically, this is your starting point for your actual “business” implementation (although I encourage calling a Business Layer from here):

Step 2: Create a simple Route handler to return the IHttpHandler:

Step 3: Register the new route in Global.asax:

That’s all there is to it. Now you can use a route for targeting your handler, for example:

http://<server>/handler/123

UPDATE: For routing Web Services (asmx), click here.

Route Data

One last issue is in cases that you perform url rewriting or other processing, and you suddenly notice that your Route Data is gone. This is quite frustrating as you can plainly see that url in the browser’s address bar. Fortunately, the solution is quite simple and also uses a Route Handler. All we have to do, is copy the Route Data in a custom Route handler to HttpContext.Current.Items collection, and we have them for the entire http request. Two steps here:

Step 1: Write a simple custom Route handler which copies the route data:

Step 2: Configure Global.asax accordingly:

To sum up – it’s quite cool to be able to use Routing in WebForms. I just finished a small project where I used Routing and the result is quite awesome. It wasn’t just looking better, but it also allowed me to “white label” the website dynamically. For example, I could do the following: http://server/productA/login and http://server/productB/login, and it really felt like two different websites.

 
4 Comments

Posted by on 22/06/2011 in Software Development

 

Tags: , ,

MVC and posting data using Html.BeginForm and Url Routing

We all have pages which represent “fill in” data forms. Quite common is a user’s details form: what’s your first name, what’s your last name, email, date of birth etc. This can be coded in various ways, but I thought that this would be a good way to get to know ASP.NET MVC’s Html.BeginForm( ). While there’s also an Ajax.BeginForm(), I think that the two may differ mainly in the desired behavior after you have completed posting and saving the form data. For example, if you’d like to redirect your user to some other view, or display an entirely new View altogether, you might prefer using Html.BeginForm( ). However, you may want to simply display an informatory “Saved successfully” message, and in that case you’d probably consider using Ajax.BeginForm( ) or simply jQuery’s post.

The thing is that Routing could make this a little tricky (as it was in my case). If a certain Route led to a Controller and resulted in a View with a “fill in” details form, the generated html Form’s Action attribute is going to be that exact route which you originally used. So how on earth are you supposed to save your data, if the same route was used to originally render your details in the first place? Sounds confusing? I also thought so till I figured it out. Lets clarify this issue with a real world example. Suppose your website allows users to view and edit their personal profile. Your route could be like so:

Users would be able to use: http://<server or site name>/profile/myUsername to view and edit their profile. Our server side code residing in the AccountController is the following:

Note that the controller has a LoadProfile action method (which corresponds to the route action shown earlier), and a SaveProfile action method. In order to have the form data posted to SaveProfile, we need to provide some arguments to Html.BeginForm( ):

Basically, this works. The View renders fine, the submit button posts the data to SaveProfile as expected and everything seems great. But, I’m only missing one thing: I don’t have the original username from which the Form originally rendered. In order words, I have no idea at this time to which user the filled in data relates to. This was available from the Route url when the profile was loaded, but it’s unavailable now, when Html.BeginForm( ) uses custom data for posting. I thought that this would be easy to accomplish, because the Html.BeginForm( ) has overloads which accept routing data, but it turned out that when I provided those, other Routes I have in my application got prioritized and this gets ugly, because it means that I’ll have to provide more routes or route constraints in order for this to work. And this was just one form in my app, and it doesn’t make sense that I’ll have to do this for all the forms I’ll develop.

This led me to a different question altogether: Why should I explicitly supply BeginForm( ) with an action or controller in the first place? How come that in MVC’s templates there aren’t such arguments when calling Html.BeginForm( ), and yet when posting data, the correct action is invoked? I noticed that if I provided no arguments to Html.BeginForm( ), the default action was a “profile/{username}” route, just like what I used for loading the user’s profile. I was puzzled over this, because I didn’t understand how MVC was supposed to differentiate between loading a profile and saving a profile, as the Route used for both operations is identical. I looked up MVC’s template and found an example for ChangePassword. Turns out that there are a couple of ChangePassword( ) overloads. The first overload received no arguments (for “loading” the form), but the second overload received a Model with the data (“saving” the form). So how does the route knows which method should be invoked?

The answer lies in an [HttpPost] attribute which is placed over the “saving” action method. The route is quite clever and the action type (either GET or POST) is used to differentiate between actions of the same name. This makes sense, because usually a GET operation is to be used for loading a form, and a POST operation is usually used to posting and saving a form. I went ahead and tried it: I removed the arguments from Html.BeginForm( ), changed LoadProfile and SaveProfile action methods simply to “Profile“, added [HttpPost] to the Profile action method which performs the saving operation, and changed the route accordingly. Here’s how this looks:

If we use two action methods by the same name without [HttpPost] on one of them, the run-time throws a “The current request for action ‘Profile’ on controller type ‘AccountController’ is ambiguous between the following action methods” exception.

Now I could use the RouteData collection for retrieving the user name, as well as other form fields which were sent over as arguments.

 
4 Comments

Posted by on 09/05/2011 in Software Development

 

Tags: , , , , , ,

ASP.NET MVC Routing

Url Routing is a cool feature that MVC depends upon. In this video, Scott Hanselman refers to the url routing as the “unsung hero” of MVC. He also demonstrated a Url Routing Debugger which will be discussed here. While I’m sure that there are quite a few things to url routing, in this post I’ll focus on what is probably a typical use of Url Routing.

Quite simply, in MVC the Url Routing’s job is to get you to the desired controller and invoke the correct method (action). Routing is performed in the Global.asax with a certain default provided by MVC templates. When you open a new ASP.NET MVC 3 project, Global.asax has a RegisterRoutes method which looks like this:

1: public static void RegisterRoutes(RouteCollection routes)
2: {
3:     routes.IgnoreRoute("{resource}.axd/{*pathInfo}" );
4:
5:     routes.MapRoute(
6:         "Default" , // Route name 
7:         "{controller}/{action}/{id}", // URL with parameters 
8:         new  { controller = "Home", action = "Index", id = UrlParameter .Optional } // Parameter defaults 
9:     );
10: }

This represents the basics. The MapRoute method presented by default consists of a “route name” (which can be anything), a url pattern (that’s the important stuff) and defaults. The url specified as the default basically means that any url in the form of http://<server>/controller/action/id will be understood as the controller to redirect to, the method to invoke and a parameter to that action. For example, a url of http://<server>/scores/list/1 will be interpreted as “go to the scoresController class, run the ‘list’ method with an id of 1”. The defaults argument in this case represents a situation where some of the url’s items are missing. So, having a url of http://<server&gt;, http://<server>/home, http://<server>/home/index or http://<server>/home/index/1 will all go to home controller and invoke the index method. The route uses the defaults to make up for missing url parameters. On the other hand, http://<server>/scores or http://<server>/home/edit will result at this stage in a 404 “resource cannot be found”, because there’s no scores controller, and no edit method in home controller (as long as we don’t add them, of course). One final thing regarding the default route: as long as you require only the most basic routing of http://<server>/controller/action and optional id, this route will be sufficient for your requests.

Adding additional routes means that you have custom route requirements which should be handled. These would probably be custom Urls to handle, which should direct the user to the different controllers and actions. In order to ensure that you’ve configured your routes correctly, it’s best to familiarize yourself with the Routing Debugger. In short, what this assembly does, is to render information summarizing the routes configured and how each url you test is being handled by those routes. In order to use the debugger, just download it from the blog above, extract it and reference the RouteDebugger.dll. After that you enter a single line of code in Application_Start (Line 8 below):

1: protected void Application_Start()
2: {
3:     AreaRegistration.RegisterAllAreas();
4:
5:     RegisterGlobalFilters(GlobalFilters.Filters);
6:     RegisterRoutes(RouteTable.Routes);
7:
8:     RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
9: }

Any url typed in will now be reflected in the rendered html, with a “true”/”false” indication as to which route handles it. This makes it so much easier to understand routing. The picture below is a part of the information rendered concerning the routing for a default url (‘/’). It’s quite clear that the first row ignores the url, and that the second row of the table is the first route which corresponds to the url (and it is the route that will be invoked in this case). The last route is added automatically and will “catchall” the routes.

Now we can add our custom routes. For example, if we wish the user to be able to enter the following url: http://<server>/scores/list/123, this can be done like so (lines 5-9):

1: public static void  RegisterRoutes(RouteCollection  routes)
2: {
3:     routes.IgnoreRoute("{resource}.axd/{*pathInfo}" );
4:
5:     routes.MapRoute(
6:         "scores" ,
7:         "scores/list/{id}" ,
8:         new { controller = "Scores" , action = "List"  }
9:     );
10:
11:     routes.MapRoute(
12:         "Default" , // Route name 
13:         "{controller}/{action}/{id}" , // URL with parameters 
14:         new { controller = "Home" , action = "Index" , id = UrlParameter .Optional } // Parameter defaults 
15:     );
16: }

It’s important to notice that as our custom route refers to a hard coded controller (“scores”) and action (“list”), we had to supply the controller and action parameters in Line 8, which will instruct the route which controller and action will be invoked.

The route debugger will show:


Because our custom route was placed above the default route, we can see in the debugger that row 2 will be the first to capture our Url. This means that there’s an importance to the order routes are being set. Routes preceding other routes will take precedence even if they all answer to the same Url.

When you’re done with the route debugger, simply comment out the line and recompile in order to proceed with the development.

As for the Url’s arguments (e.g. “id”), the controller’s action can either handle these via method arguments, or by using the RouteData.Values dictionary, but you can’t have both at the same time or you’ll get a AmbiguousMatchException (“The current request for action … on controller type … is ambiguous between the following action methods”):

1: public ActionResult List(int id)
2: {
3:     // code here ... 
4:
5:     return View();
6: }

 

1: public ActionResult List()
2: {
3:     int id = Convert.ToInt32(RouteData.Values["id"]);
4:
5:     // code here ... 
6:
7:     return View();
8: }

 

One more thing I think is worth mentioning is handling “greedy routes” and constraints. As you can easily observe, routes can easily become “greedy”, which means that they can pick up Urls that you basically intended for them to skip, in order for a different route to handle them. For example, the default route is quite greedy, as almost any url you enter will be picked up by it (whether or not you actually have a controller and action to handle them). You may also come across situations that you’d like the user to have several Urls which are very similar to each other, but they should be handled by different controllers or actions. For example, a Url can easily have an argument of a textual type and an integer type. So you might want different controllers or actions to handle them according to their data types. In all these cases you have the option of contraining a route using regular expressions. By constraining a route, routes will become less greedy and can be skipped over. As an example, lets assume that the Urls need to differ so that one of the parameters can be either a string or an integer, but with different controllers handling each one.

1: routes.MapRoute( // http://<server>/scores/3 
2:     "int scores" ,
3:     "scores/{id}" ,
4:     new { controller = "Scores" , action = "IntegerAction"  },
5:     new { id = @"\d+" }
6: );
7:
8: routes.MapRoute( // http://<server>/scores/a2 
9:     "text scores" ,
10:     "scores/{id}" ,
11:     new { controller = "Scores" , action = "TextualAction"  },
12:     new { id = "[A-Za-z].+" }
13: );

In lines 1 & 8 you can see examples of the Urls being handled. Note that we may omit line 12 and have the second route “greedier”, because we know that the first route will handle integers anyhow. Naturally, you can have different controllers assigned to handle each route.

Finally, if you insist on having actions (i.e. methods) of the same name with different overloads to handle the routes, you can place a single route to handle both integers and strings, and differentiate between the overloads using attributes, as shown here. I’m not sure why this is better than having constraints on your routes, but it’s always good to know your alternatives.

 
4 Comments

Posted by on 27/03/2011 in Software Development

 

Tags: , , ,