views:

175

answers:

3

I have some REST services using plain old IHttpHandlers. I'd like to generate cleaner URLs, so that I don't have the .ashx in the path. Is there a way to use ASP.NET routing to create routes that map to ashx handlers? I've seen these types of routes previously:

// Route to an aspx page
RouteTable.Routes.MapPageRoute("route-name",
    "some/path/{arg}",
    "~/Pages/SomePage.aspx");

// Route for a WCF service
RouteTable.Routes.Add(new ServiceRoute("Services/SomeService",
    new WebServiceHostFactory(),
    typeof(SomeService)));

Trying to use RouteTable.Routes.MapPageRoute() generates an error (that the handler does not derive from Page). System.Web.Routing.RouteBase only seems to have 2 derived classes: ServiceRoute for services, and DynamicDataRoute for MVC. I'm not sure what MapPageRoute() does (Reflector doesn't show the method body, it just shows "Performance critical to inline this type of method across NGen image boundaries").

I see that RouteBase is not sealed, and has a relatively simple interface:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
    RouteValueDictionary values);

So perhaps I can make my own HttpHandlerRoute. I'll give that a shot, but if anyone knows of an existing or built-in way of mapping routes to IHttpHandlers, that would be great.

+1  A: 

Yeah, I noticed that, too. Perhaps there is a built-in ASP.NET way to do this, but the trick for me was to create a new class derived from IRouteHandler:

using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;

namespace MyNamespace
{
    class GenericHandlerRouteHandler : IRouteHandler
    {
        private string _virtualPath;
        private Type _handlerType;
        private static object s_lock = new object();

        public GenericHandlerRouteHandler(string virtualPath)
        {
            _virtualPath = virtualPath;
        }

        #region IRouteHandler Members

        public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            ResolveHandler();

            IHttpHandler handler = (IHttpHandler)Activator.CreateInstance(_handlerType);
            return handler;
        }

        #endregion

        private void ResolveHandler()
        {
            if (_handlerType != null)
                return;

            lock (s_lock)
            {
                // determine physical path of ashx
                string path = _virtualPath.Replace("~/", HttpRuntime.AppDomainAppPath);

                if (!File.Exists(path))
                    throw new FileNotFoundException("Generic handler " + _virtualPath + " could not be found.");

                // parse the class name out of the .ashx file
                // unescaped reg-ex: (?<=Class=")[a-zA-Z\.]*
                string className;
                Regex regex = new Regex("(?<=Class=\")[a-zA-Z\\.]*");
                using (var sr = new StreamReader(path))
                {
                    string str = sr.ReadToEnd();

                    Match match = regex.Match(str);
                    if (match == null)
                        throw new InvalidDataException("Could not determine class name for generic handler " + _virtualPath);

                    className = match.Value;
                }

                // get the class type from the name
                Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly asm in asms)
                {
                    _handlerType = asm.GetType(className);
                    if (_handlerType != null)
                        break;
                }

                if (_handlerType == null)
                    throw new InvalidDataException("Could not find type " + className + " in any loaded assemblies.");
            }
        }
    }
}

To create a route for an .ashx:

IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);

The code above may need to be enhanced to work with your route arguments, but it's starting point. Comments welcome.

Joel
+2  A: 

Ok, I've been figuring this out since I originally asked the question, and I finally have a solution that does just what I want. A bit of up front explanation is due, however. IHttpHandler is a very basic interface:

bool IsReusable { get; }
void ProcessRequest(HttpContext context)

There is no built in property for accessing the route data, and the route data also can't be found in the context or the request. A System.Web.UI.Page object has a RouteData property , ServiceRoutes do all the work of interpreting your UriTemplates and passing the values to the correct method internally, and ASP.NET MVC provides its own way of accessing the route data. Even if you had a RouteBase that (a) determined if the incoming url was a match for your route and (b) parsed the url to extract all of the individual values to be used from within your IHttpHandler, there's no easy way to pass that route data to your IHttpHandler. If you want to keep your IHttpHandler "pure", so to speak, it takes responsibility for dealing with the url, and how to extract any values from it. The RouteBase implementation in this case is only used to determine if your IHttpHandler should be used at all.

One problem remains, however. Once the RouteBase determines that the incoming url is a match for your route, it passes off to an IRouteHandler, which creates the instances of the IHttpHandler you want to handle your request. But, once you're in your IHttpHandler, the value of context.Request.CurrentExecutionFilePath is misleading. It's the url that came from the client, minus the query string. So it's not the path to your .ashx file. And, any parts of your route that are constant (such as the name of the method) will be part of that execution file path value. This can be a problem if you use UriTemplates within your IHttpHandler to determine which specific method within your IHttpHandler should handing the request.

Example: If you had a .ashx handler at /myApp/services/myHelloWorldHandler.ashx And you had this route that mapped to the handler: "services/hello/{name}" And you navigated to this url, trying to call the SayHello(string name) method of your handler: http://localhost/myApp/services/hello/SayHello/Sam

Then your CurrentExecutionFilePath would be: /myApp/services/hello/Sam. It includes parts of the route url, which is a problem. You want the execution file path to match your route url. The below implementations of RouteBase and IRouteHandler deal with this problem.

Before I paste the 2 classes, here's a very simple usage example. Note that these implementations of RouteBase and IRouteHandler will actually work for IHttpHandlers that don't even have a .ashx file, which is pretty convenient.

// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));

That will cause all incoming urls that match the "services/headless" route to be handed off to a new instance of the HeadlessService IHttpHandler (HeadlessService is just an example in this case. It would be whatever IHttpHandler implementation you wanted to pass off to).

Ok, so here are the routing class implementations, comments and all:

/// <summary>
/// For info on subclassing RouteBase, check Pro Asp.NET MVC Framework, page 252.
/// Google books link: http://books.google.com/books?id=tD3FfFcnJxYC&amp;pg=PA251&amp;lpg=PA251&amp;dq=.net+RouteBase&amp;source=bl&amp;ots=IQhFwmGOVw&amp;sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&amp;hl=en&amp;ei=z61UTMKwF4aWsgPHs7XbAg&amp;sa=X&amp;oi=book_result&amp;ct=result&amp;resnum=6&amp;ved=0CC4Q6AEwBQ#v=onepage&amp;q=.net%20RouteBase&amp;f=false
/// 
/// It explains how the asp.net runtime will call GetRouteData() for every route in the route table.
/// GetRouteData() is used for inbound url matching, and should return null for a negative match (the current requests url doesn't match the route).
/// If it does match, it returns a RouteData object describing the handler that should be used for that request, along with any data values (stored in RouteData.Values) that
/// that handler might be interested in.
/// 
/// The book also explains that GetVirtualPath() (used for outbound url generation) is called for each route in the route table, but that is not my experience,
/// as mine used to simply throw a NotImplementedException, and that never caused a problem for me.  In my case, I don't need to do outbound url generation,
/// so I don't have to worry about it in any case.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericHandlerRoute<T> : RouteBase where T : IHttpHandler, new()
{
    public string RouteUrl { get; set; }


    public GenericHandlerRoute(string routeUrl)
    {
        RouteUrl = routeUrl;
    }


    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // See if the current request matches this route's url
        string baseUrl = httpContext.Request.CurrentExecutionFilePath;
        int ix = baseUrl.IndexOf(RouteUrl);
        if (ix == -1)
            // Doesn't match this route.  Returning null indicates to the asp.net runtime that this route doesn't apply for the current request.
            return null;

        baseUrl = baseUrl.Substring(0, ix + RouteUrl.Length);

        // This is kind of a hack.  There's no way to access the route data (or even the route url) from an IHttpHandler (which has a very basic interface).
        // We need to store the "base" url somewhere, including parts of the route url that are constant, like maybe the name of a method, etc.
        // For instance, if the route url "myService/myMethod/{myArg}", and the request url were "http://localhost/myApp/myService/myMethod/argValue",
        // the "current execution path" would include the "myServer/myMethod" as part of the url, which is incorrect (and it will prevent your UriTemplates from matching).
        // Since at this point in the exectuion, we know the route url, we can calculate the true base url (excluding all parts of the route url).
        // This means that any IHttpHandlers that use this routing mechanism will have to look for the "__baseUrl" item in the HttpContext.Current.Items bag.
        // TODO: Another way to solve this would be to create a subclass of IHttpHandler that has a BaseUrl property that can be set, and only let this route handler
        // work with instances of the subclass.  Perhaps I can just have RestHttpHandler have that property.  My reticence is that it would be nice to have a generic
        // route handler that works for any "plain ol" IHttpHandler (even though in this case, you have to use the "global" base url that's stored in HttpContext.Current.Items...)
        // Oh well.  At least this works for now.
        httpContext.Items["__baseUrl"] = baseUrl;

        GenericHandlerRouteHandler<T> routeHandler = new GenericHandlerRouteHandler<T>();
        RouteData rdata = new RouteData(this, routeHandler);

        return rdata;
    }


    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        // This route entry doesn't generate outbound Urls.
        return null;
    }
}



public class GenericHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new T();
    }
}

I know this answer has been quite long winded, but it was not an easy problem to solve. The core logic was easy enough, the trick was to somehow make your IHttpHandler aware of the "base url", so that it could properly determine what parts of the url belong to the route, and what parts are actual arguments for the service call.

These classes will be used in my upcoming C# REST library, RestCake. I hope that my path down the routing rabbit hole will help anyone else who decides to RouteBase, and do cool stuff with IHttpHandlers.

Samuel Meacham
A: 

I actually like Joel's solution better, as it doesn't require you to know the type of handler while you're trying to setup your routes. I'd upvote it, but alas, I haven't the reputation required.

I actually found a solution which I feel is better than both mentioned. The original source code I derived my example from can be found linked here http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx.

This is less code, type agnostic, and fast.

public class HttpHandlerRoute : IRouteHandler {

  private String _VirtualPath = null;

  public HttpHandlerRoute(String virtualPath) {
    _VirtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    IHttpHandler httpHandler = (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(_VirtualPath, typeof(IHttpHandler));
    return httpHandler;
  }
}

And a rough example of use

String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));
shellscape
While you may not have to know the type of the IHttpHandler implementation, you do have to know the path to the ashx file. My solution doesn't require an ashx file at all, and it's a compile time check. Your solution will cause a runtime error if the setup is bad. It's just as easy to refer to the type of a handler as it is to refer to the path of the ashx. Safer too. It is much easier to understand though, and that's nice. Simple is good. I'm gonna stick with mine just the same, because I like not having to have the ashx file. I just create a plain class that implements IHttpHandler.
Samuel Meacham