This article will discuss the use of generic handlers, such as .ashx files, in a web project using ASP.NET routing. This is article is based on experience with .NET 4.0 Beta 1 framework. It will probably not apply to .NET 3.5 projects, and although I believe the general concept will still be valid in future versions of ASP.NET, the next version may contain a fix that makes a workaround described here obsolete.
Should any of the functionality described in here be affected by subsequent betas or the final version of .NET 4.0 I will keep this post updated.
The Generic Handler
ASP.NET has long had the option for creating a file that could serve other data than HTML. This could be images generated dynamically, or files that could come from a binary field in a database, or maybe runtime compression of javascript files, etc. The uses are many.
The easy way to implement such a generic handler would be to create an .ashx file, and implement the process request. You could also write a class directly that implements the IHttpHandler interface, and then register this file with a specific extension for example.
In both cases, you need to implement the IHttpHandler interface.
public interface IHttpHandler { void ProcessRequest(HttpContext context); bool IsReusable { get; } }
The interesting function if ProcessRequest where the implementation writes the actual data, text or binary, to the output.
With the routing feature in .NET 4.0, it would be natural to map a route to either the .ashx file, or just a class that implements the IHttpHandler interface. The first case is not directly possible unfortunately, but the second case is very easy to implement. And compared to pre-routing possibilities, you don't need to create mappings in web.config.
I have not really spend a long time investigating the option of getting .ashx files to work with routing because I'm much more happy with just having a class implementing IHttpHandler. There therefore may be easy solutions possible for this scenario. But if there was, I wouldn't use them.
Why can't we use .ashx files
In the .NET 4.0 Beta 1 framework you have to add routes to the route table, using
Routes.Add(string routeName, RouteBase route)
The RouteBase class is an abstract class in the framework, and there is only one concrete implementation, the Route class. This class is constructed through one of the constructors:
public Route(string url, IRouteHandler routeHandler); public Route(string url, RouteValueDictionary defaults, IRouteHandler routeHandler); public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler); public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler);
In every case you need an object that implements IRouteHandler. Again, only one implementation of this interface is implemented, the PageRouteHandler that easily routes to an .aspx file. This handler does not accept an .ashx file however, as it is not a Page.
So to use .ashx files, you'd need the non-existing AshxRouteHandler. But since I don't miss it, I haven't spend time investigating how difficult it would be.
Implementing Routes for an IHttpHandler implementation
Since I have dismissed the usage of .ashx files, I will now go into the solution that I use and like, creating a route for a class that implements the IHttpHandler interface. To accomplish this, all we need to do is create a class that implements the IRouteHandler. This interface only has a single function, that is quite easy to implement.
// This is the actual http handler, the class that should handle the actual requests. public class MyHandler : IHttpHandler { ... } public class MyHandlerRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new MyHandler(); } } // Map the route to extract an image with a specific ID // This code is added where all the other routes are mapped. RouteTable.Routes.Add(new Route("Image/{ImageID}", new MyHandlerRouteHandler()));
That is basically be it. There is a simple problem however. We cannot inspect the route values in our handler. Normally you would retrieve the route values like this
public class MyHandler { public void ProcessRequest(HttpContext context) { int imageID = int.Parse((string)context.Request.RequestContext.RouteData.Values["ImageID"]); } }
The Request.RequestContext property is not initialized however.
If you use reflector to reverse engineer and inspect the .NET framework you can see that the PageRouteHandler class injects this RequestContext
public virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { ... page.Context.Request.RequestContext = requestContext; ... }
But because the RequestContext property setter is marked as internal, we cannot do this in our own implementation of IHttpHandler, so we have to create a workaround for this (or maybe more correctly, a different workaround than the MS one). I would argue that this behaviour is not logical, and the RequestContext property should be set automatically by the framework. Hopefully this will be changed before the release. Here is a modified implementation of the above code that correctly retrieves the route data.
public class MyHandler : IHttpHandler { public RequestContext RequestContext { get; set; } public void ProcessRequest(HttpContext context) { int imageID = int.Parse((string)RequestContext.RouteData.Values["ImageID"]); ... } } public class MyHandlerRouteHandler : IRouteHandler { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new MyHandler() {RequestContext = requestContext}; } }
Generic Solution Without a DI container
Here I will present a solution for a generic route handler so you don't need to create a new route handler class for each http handler class in the system. This solution has some requirements that are unnecessary if you are using a DI (Direct Injection) container to control instantiation of your classes.
public interface IHttpHandlerBase : IHttpHandler { void RequestContext {get; set; } } public class GenericRouteHandler<T> : IRouteHandler where T : IHttpHandlerBase, new() { public IHttpHandler GetHttpHandler(RequestContext requestContext) { var retVal = new T(); retVal.RequestContext = requestContext; return retVal; } } public class ImageHandler : IHttpHandlerBase { ... } // This goes into the route initialization RouteTable.Routes.Add(new Route("Image/{ImageID}", new GenericRouteHandler<ImageHandler>()));
Generic solution with a DI container
If you are using a DI container, then you can remove the need for creating a new interface. It also removes the need for the where new() generic constraint. I prefer using StructureMap as DI container. Here is my implementation for a generic route handler using StructureMap.
public class MyImageHandler : IHttpHandler { RequestContext _requestContext; public MyImageHandler(RequestContext requestContext, // Other dependencies that the class needs ) { _requestHandler = requestHandler; // Store other dependencies } public void ProcessRequest(HttpContext context) { ... } } public class GenericRouteHandler<T> : IRouteHandler where T : IHttpHandler { public IHttpHandler(RequestContext requestContext) { return ObjectFactory.With(requestContext).GetInstance<T>(); } } // Route initialization RouteTable.Routes.Add(new Route("Image/{ImageID}", new GenericRouteHandler<MyImageHandler>()));
The With() function tells StructureMap that during this specific call, this particular instance of the RequestContext should be used for any constructor parameter of that particular type. This avoids having to create plugin rules for RequestContext in structure map.
This class, as it is written here, is the one that I use in my own system.
Final Thoughts
I've been working with the ASP.NET 4.0 Beta 1 for some months now, and I really appreciate the routing feature build in. I find that this works more smoothly than other 3rd party url rewriting modules that I've investigated. This is likely because there is build in support in the core of the framework. Some 3rd party url rewriting modules required some workarounds in order to be able to handle postbacks correctly. With the build in routing this is never a problem.
And with very little work it is easy to extend this functionlity to not only work with .aspx pages, but any http handler in the system.
Pete
What i dont get is how the instance of the IHttpHandler can be reused (when IsReuseable returns true), when it is created for each request, by the IRouteHandler?
ReplyDeleteThis is so great! Thanks alot you saved me so much time.
ReplyDeleteAdding the Route appears to mess up the route mapping, and so confuses MVC Html helpers when, say using @Html.BeginForm in the views. See http://stackoverflow.com/questions/23115459/httphandler-and-routes-in-asp-net-mvc.
ReplyDeletei didn't get it, can you make video section of this article with example
ReplyDelete