I have some REST services using plain old IHttpHandler
s. 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.
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:
To create a route for an .ashx:
The code above may need to be enhanced to work with your route arguments, but it's starting point. Comments welcome.
All of these answers are very good. I love the simplicity of Mr. Meacham's
GenericHandlerRouteHandler<T>
class. It is a great idea to eliminate an unnecessary reference to a virtual path if you know the specificHttpHandler
class. TheGenericHandlerRoute<T>
class is not needed, however. The existingRoute
class which derives fromRouteBase
already handles all of the complexity of route matching, parameters, etc., so we can just use it along withGenericHandlerRouteHandler<T>
.Below is a combined version with a real-life usage example that includes route parameters.
First are the route handlers. There are two included, here -- both with the same class name, but one that is generic and uses type information to create an instance of the specific
HttpHandler
as in Mr. Meacham's usage, and one that uses a virtual path andBuildManager
to create an instance of the appropriateHttpHandler
as in shellscape's usage. The good news is that .NET allows both to live side by side just fine, so we can just use whichever we want and can switch between them as we wish.Let's assume that we created an
HttpHandler
that streams documents to users from a resource outside our virtual folder, maybe even from a database, and that we want to fool the user's browser into believing that we are directly serving a specific file rather than simply providing a download (i.e., allow the browser's plug-ins to handle the file rather than forcing the user to save the file). TheHttpHandler
may expect a document id with which to locate the document to provide, and may expect a file name to provide to the browser -- one that may differ from the file name used on the server.The following shows the registration of the route used to accomplish this with a
DocumentHandler
HttpHandler
:I used
{*fileName}
rather than just{fileName}
to allow thefileName
parameter to act as an optional catch-all parameter.To create a URL for a file served by this
HttpHandler
, we can add the following static method to a class where such a method would be appropriate, such as in theHttpHandler
class, itself:I omitted the definitions of
MimeMap
and andIsPassThruMimeType
to keep this example simple. But these are intended to determine whether or not specific file types should provide their file names directly in the URL, or rather in aContent-Disposition
HTTP header. Some file extensions could be blocked by IIS or URL Scan, or could cause code to execute that might cause problems for users -- especially if the source of the file is another user who is malicious. You could replace this logic with some other filtering logic, or omit such logic entirely if you are not exposed to this type of risk.Since in this particular example the file name may be omitted from the URL, then, obviously, we must retrieve the file name from somewhere. In this particular example, the file name can be retrieved by performing a look-up using document id, and including a file name in the URL is intended solely to improve the user's experience. So, the
DocumentHandler
HttpHandler
can determine if a file name was provided in the URL, and if it was not, then it can simply add aContent-Disposition
HTTP header to the response.Staying on topic, the important part of the above code block is the usage of
RouteTable.Routes.GetVirtualPath()
and the routing parameters to generate a URL from theRoute
object that we created during the route registration process.Here's a watered-down version of the
DocumentHandler
HttpHandler
class (much omitted for the sake of clarity). You can see that this class uses route parameters to retrieve the document id and the file name when it can; otherwise, it will attempt to retrieve the document id from a query string parameter (i.e., assuming that routing was not used).This example uses some additional custom classes, such as a
Utility
class to simplify some trivial tasks. But hopefully you can weed through that. The only really important part in this class with regard to the current topic, of course, is the retrieval of the route parameters fromcontext.Request.RequestContext.RouteData
. But I've seen several posts elsewhere asking how to stream large files using anHttpHandler
without chewing up server memory, so it seemed like a good idea to combine examples.EDIT: I just edited this code because I had some issues with the old one. If you're using the old version please update.
This thread is a bit old but I just re-wrote some of the code here to do the same thing but on a more elegant way, using an extension method.
I'm using this on ASP.net Webforms, and I like to have the ashx files on a folder and being able to call them either using routing or a normal request.
So I pretty much grabbed shellscape's code and made an extension method that does the trick. At the end I felt that I should also support passing the IHttpHandler object instead of its Url, so I wrote and overload of the MapHttpHandlerRoute method for that.
I'm putting it inside the same namespace of all the native routing objects so it will be automatically available.
So to use this you just have to call:
Or
Enjoy, Alex
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:
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 aRouteData
property ,ServiceRoute
s 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 aRouteBase
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/SamThen 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 ofRouteBase
andIRouteHandler
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.
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:
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.
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.
And a rough example of use