Working with Custom HTTP Handlers
Looking under the hood at ASP.NET MVC routes
December 1, 2009
During an ASP.NET MVC class I taught recently, I picked up a question about Convention over Configuration (CoC) and how (and where) it applies to ASP.NET MVC. CoC is a development paradigm designed to reduce the number of decisions made during a project. The paradigm is not a philosophy that inspires architectural decisions. It is, instead, all about increasing simplicity without sacrificing flexibility. A convention makes assumptions about the code. If you follow convention, you don’t need to write configuration information anywhere. If you don’t follow convention, you write only what differs in some external file. In ASP.NET MVC, for example, convention tells you where and how to write controller and view classes.
Convention is defined as a usual or accepted way of behaving, often following an old way of thinking. In classic ASP.NET, the usual way of writing pages is authoring an ASPX markup file and corroborating it with a twin class in the code-behind file. When a request is made to an ASP.NET application, the runtime environment resolves it in terms of a disk-based file whose content is processed and determines the response. By convention, ASP.NET Web Forms is a strictly file-based framework. But you can make configuration win over convention by simply taking the right steps.
In this article, I’ll contrast classic Web Forms requests with HTTP handlers and then discuss some of the intricacies you might face when hosting custom HTTP handlers in an ASP.NET MVC application.
HTTP Handlers in ASP.NET
Each ASP.NET request is mapped to a special component known as the HTTP handler. The ASP.NET runtime uses a built-in algorithm to determine the HTTP handler in charge for a given ASP.NET request.
In Web Forms, this algorithm is based on the URL of the requested page, meaning that you'll have a different HTTP handler for each distinct URL you request. If you requested, say, page.aspx, then the HTTP handler is a class named ASP.page_aspx that inherits from the code-behind class you specified in the source code of page.aspx.cs.
If you're missing the link between ASP.NET pages and an HTTP handler, note that an HTTP handler is any class that implements the IHttpHandler interface. Furthermore, the base class System.Web.UI.Page, from which any code-behind class directly or indirectly inherits, is just an HTTP handler.
Convention says that in Web Forms a request is aimed at a server disk file. An ASP.NET MVC request is, instead, based on a free-form URL. Because both platforms share the same runtime environment, it turns out that free-form URLs in ASP.NET MVC are possible only because of ASP.NET runtime's flexibility.
In a plain ASP.NET Web Forms application you can place requests for any URL you want according to any syntax you wish. If you intend to follow the ASP.NET Web Forms convention, you don’t need to make anything special other than authoring pages. If you intend to violate the convention and support requests with a custom syntax, you have to tweak the configuration—specifically the web.config file.
The code in Figure 1 shows one possible way of registering an HTTP handler that works in a classic Web Forms application.
Figure 1. Registering an HTTP handler via an extension-based URL
path="hello.axd"
type="Samples.Components.SimpleHandler" />
Note that the endpoint hello.axd is simply a public resource identifier; it doesn’t have to be a physical resource on the server, such as a file. Nor does it have to end with the .axd extension. The endpoint can be any string that the target handler knows how to process. The endpoint could also be something like the following:
Customers/ALFKI/orders/usa
The URL doesn’t point to a file, but it contains information for the handler to process. At the end of the day, this is just what happens in ASP.NET MVC where a specialized HTTP handler determines which controller to invoke from the URL.
Handlers in ASP.NET MVC
If you add the configuration of Figure 1 to an ASP.NET MVC application, it will work like a charm. In a Web Forms application, though, you can easily use any extensions to characterize the HTTP endpoint for the handler, including the well-known .aspx extension. Now try changing the path hello.axd in Figure 1 to hello.aspx.
Interestingly enough, a request for hello.aspx in Web Forms works well regardless of whether such a server resource really exists. The registered handler captures the request and processes it in a nonconventional way.
While it works as expected in a Web Forms application, it will return a nasty HTTP 404 error code if used in the context of an ASP.NET MVC application. To add even more spice, any handlers registered to an .axd and .ashx extension work just fine in ASP.NET MVC.
When you request hello.aspx, the Web server returns a plain HTTP 404, as you can see in the top half of Figure 2. However, by opening the browser's HTML source window you can figure out what went wrong with ASP.NET MVC.
The standard HTTP 404 page embeds debugging information that turns out to be quite helpful for figuring out the mechanics of the error. The inner HTTP exception raised on the server just says
The controller for path '/hello.aspx' could not be found or it does not implement IController.
The message is definitely clear, but does it really explain it all? Well, not entirely. The reason for the failure has to be researched because you don’t have a hello.aspx file in ASP.NET MVC. In ASP.NET MVC, a request for hello.aspx is in some way automatically mapped to a physical matching file; if that file doesn’t exist, you get an HTTP 404 exception. Try adding to the server a file—any file, regardless of the content—with a name of hello.aspx and reiterate the request. To your surprise, it will now work smoothly. Apparently, it seems to be a matter of extension-based URLs in ASP.NET MVC. By supposing so, you won’t go too far from the truth. But before coming to a conclusion let’s experiment more.
Handlers with an AXD Extension
We have observed the following facts: If the extension is AXD, a custom HTTP handler is correctly recognized and invoked in ASP.NET MVC. There’s no need to have a physical AXD file within the server. If the URL extension is renamed to ASPX, you get an HTTP 404 error unless you add a file with a matching name. The file just has to exist; no matter the content.
What if you change the extension to ASHX? In this case, and just for this extension, by design the ASP.NET infrastructure forces us to have a physical endpoint with that name.
All in all, it still remains unclear why a request for an .axd endpoint works just fine even when there’s no such server file, whereas other custom HTTP handler requests fail unless a matching file exists.
This apparently odd behavior is due to the following code you find by default in the global.asax.cs file of any ASP.NET MVC application:
public static void RegisterRoutes(RouteCollection routes)
\{
routes.IgnoreRoute("\{resource\}.axd/\{*pathInfo\}");
:
\}
In ASP.NET MVC, the URL routing module intercepts any request and matches it to the list of application routes defined in RegisterRoutes. You can use the IgnoreRoute method to skip over all URLs that match the pattern. The net effect of the preceding code is that any requests whose URL ends with the AXD extension is ignored by the ASP.NET MVC runtime. These requests then fall back to the classic ASP.NET Web Forms handler and are processed as usual.
A little explanation is required for the \{*pathInfo\} placeholder in the route expression. The token pathInfo simply represents a placeholder for any content following the .axd URL. The asterisk (*), though, indicates that the last parameter should match the rest of the URL. In other words, anything that follows the .axd extension goes into the pathInfo parameter. Such parameters are referred to as catch-all parameters.
Handling Requests for Physical Files
Another configurable aspect of the routing system is whether or not it needs to handle requests that match a physical file. By default, the ASP.NET routing system ignores requests whose URL can be mapped to a file that physically exists on the server. Note that if a server file exists, the routing system ignores the request even if the request matches a route.
In case of need, you can force the routing system to handle all requests by setting the RouteExistingFiles property of the RouteCollection object to true, as shown below.
// In global.asax.cs
public static void RegisterRoutes(RouteCollection routes)
\{
routes.RouteExistingFiles = true;
:
\}
Note that having all requests handled via routing may cause some issues in an ASP.NET MVC application. For example, if you add the preceding code to the global.asax.cs file of a sample ASP.NET MVC application and run it, you’ll immediately face an HTTP 404 error on default.aspx. The exception is the same as in Figure 1. If a request for default.aspx is handed over the ASP.NET MVC, it can be served only in terms of a controller; but no controller exists for path /default.aspx.
The Role of Default.aspx
What’s the real purpose of the default.aspx file and its code-behind file in an ASP.NET MVC application? The role of the file depends on the version of the IIS Web server you’re using. If you're running the application under IIS 7 in Integrated Pipeline mode, you don’t need default.aspx. You can remove that file and all of its sub-files from the project and still be happy, as long as requests for physical files are not routed to MVC.
In IIS 7 Integrated Pipeline mode, a request for the application root (i.e., http://yourserver/) is automatically captured by the routing system and processed in terms of the predefined routes. The same happens if you test the application with the embedded Web server (aka, Cassini) that comes with Visual Studio 2008 Service Pack 1 and newer versions. It goes without saying that if you specifically request default.aspx in the address bar and the file is not on the server you’ll get an HTTP 404 error.
If you're using an older version of Visual Studio, or if you're hosting the ASP.NET MVC application under IIS 6 or IIS 7 Classic mode, then default.aspx is required. In all these cases, a request for the application root (i.e., http://yourserver/) is resolved in terms of a startup document—default.aspx.
In other words, a request for the application root is not recognized as an ASP.NET MVC request under versions of IIS older than 7. For this reason, you need to have a default.aspx in your ASP.NET MVC application to capture the request. Note that the version of Cassini that comes with Visual Studio 2008 SP1 is different from that which comes with Visual Studio 2008 and is able to simulate the Integrated Pipeline mode.
I need to clarify one final point. In an ASP.NET MVC application running under IIS 6, you still need some code in the default.aspx page that invokes a method on a controller and produces a view. This doesn’t happen out of the box and achieving this is precisely the purpose of the code you find in the Page_Load method of the default.aspx code-behind class.
An Alternative Programming Model
To successfully define HTTP handlers in an ASP.NET MVC application, you register them with an AXD extension or define them through an ASHX endpoint. If you can’t avoid using an ASPX endpoint, make sure you deploy a server file with the same name as the endpoint. Such a file can have any content and can even be empty.
ASP.NET MVC offers an alternate programming model compared to Web Forms but shares the same runtime environment. This means a few tricks are necessary when it comes to leveraging low-level features such as HTTP handlers. Hopefully, this article shed some light on some little understood points.
Dino Esposito ([email protected]) is an architect at IDesign and specializes mainly in ASP.NET, AJAX and RIA solutions. Dino co-authored the best-seller Microsoft .NET: Architecting Applications for the Enterprise (Microsoft Press, 2008) and Microsoft ASP.NET and AJAX: Architecting Web Applications (Microsoft Press, 2009).
About the Author
You May Also Like