.NET 4 URL Routing for Web Services (asmx)

30 Dec

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.

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)]
public class MyWebService : System.Web.Services.WebService
    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)

    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");

The web.config looks like this:

<?xml version="1.0"?>
        <add name="HttpGet"/>
        <add name="HttpPost"/>
    <compilation debug="true" targetFramework="4.0" />

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

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

<!DOCTYPE html>
<html xmlns="">
<head runat="server">
    <script type="text/javascript" src=""></script>
    <form id="form1" runat="server">
    <script type="text/javascript">
        function doGet() {
                url: "http://localhost/Routing/" + $('#product').val() + "/xxx/HelloWorld?name=evolpin",
                type: "GET",
                success: function (result) {

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

        function doPostJson() {
                url: "http://localhost/Routing/" + $('#product').val() + "/xxx/HelloWorld",
                type: "POST",
                contentType: 'application/json',
                data: JSON.stringify({ name: 'evolpin' }),
                success: function (result) {
        <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();' />
  • 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:

  • ‘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.


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:


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.


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:


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:


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:


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:


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:


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:


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:


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:


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.

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):


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.


Posted by on 30/12/2012 in Software Development


Tags: ,

7 responses to “.NET 4 URL Routing for Web Services (asmx)

  1. Lelala

    06/07/2013 at 12:10

    Thanks for that awesome and very cool post! 🙂

  2. Luis Yoroslav

    17/10/2013 at 04:16

    Thanks a lot.

  3. Andy Merhaut (@foobarjs)

    01/08/2014 at 03:24

    A year later and this post is still gold! Thank you for sharing.

  4. ttol

    09/12/2014 at 10:53

    thanks a lot, and before RewritePath, check Request.HttpMethod !=”GET” , it can fix WSDL

  5. guest

    07/01/2015 at 02:26

    Creating a route for a .asmx Web Service with ASP.NET routing

  6. Alex

    30/07/2015 at 16:12

    What needs to be change if you want the route as this way : “{*pathInfo}/{product}/”


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: