advertisement

Rewriting the URL using IHttpHandlerFactory

posted by Jeff
Discuss this article   Printer friendly

RewritePath() is messy. Using the IHttpHandlerFactory interface allows you to take the requested URL and execute an entirely different page.

For many moons, actually since the pre-beta days of ASP.NET, developers have wanted to take an incoming request, interpret the URL, then send something back other than a file that would normally correspond with the URL. Common applications include content management systems and the elimination of "ugly" URL's with big query strings and other such nonsense.

The most popular method I've seen is to use an HttpHandler (by way of its ProcessRequest() method) or an HttpModule (by way of a new BeginRequest handler) to interpret the request and call HttpContext.Current.RewritePath(someFile) to make it happen. This works OK, but problems arise when you attempt to make a post-back to the page, since the URL in the form tag's action attribute points to the actual physical file you rewrote the path to. So for example, you might have requested /mypage.aspx, but used RewritePath() to go to /content.aspx?id=532&insanity=true, and that's what appears in the form tag. You can of course override the page's Render() method to correct this, but that's a pain too.

Exploring IHttpHandlerFactory

There is an easier way. Enter System.Web.IHttpHandlerFactory, a handy interface that allows you to interpret the request of a URL, request an instance of a particular page, and be done with it. First, let's look at the interface:

public interface IHttpHandlerFactory
{
   public IHttpHandler GetHandler(HttpContext context, string RequestType, 
      string url, string pathTranslated);
   public void ReleaseHandler(IHttpHandler handler);
}

ReleaseHandler() is used to recycle or dispose of HttpHandler objects, but that's beyond the scope of this article. GetHander() is used to return an instance of an IHttpHandler. In our case, we're going to return a handler that creates a page, but we'll get to that in a moment. It has four parameters:

You can already see a lot of goodness and potential for this interface, right? To make our class implementing IHttpHandlerFactory work, we'll have to map requests to it in web.config. Let's state our goal right here: We want to map requests for .aspx pages to the same page, content.aspx, and based on the URL, fill that page with some particular text. Using .aspx pages is a good choice because the ASP.NET runtime is already getting these requests anyway. Here's the class to do it...

using System;
using System.IO;
using System.Web;
using System.Web.UI;

public class MyPageFactory : IHttpHandlerFactory
{
   public IHttpHandler GetHandler(HttpContext context, string requestType, 
      string url, string pathTranslated)
   {
      context.Items["fileName"] = Path.GetFileNameWithoutExtension(url).ToLower();
      return PageParser.GetCompiledPageInstance(url, 
         context.Server.MapPath("~/Content.aspx"), context);
   }

   public void ReleaseHandler(IHttpHandler handler)
   {
   }
}

We start the class by indicating we'll implement the IHttpHandlerFactory interface. Our GetHandler() method has two steps to it. The first step is to identify the file name of the request, without the .aspx extension on it. If the request is for SomePage.aspx, we want the string SomePage. We'll store it in the Items collection of the current HttpContext, so we can access it later from the page.

Now we need to figure out a way to get an instance of an HttpHandler returned to the ASP.NET runtime (you'll see why the runtime is calling this class when we get to that web.config setup). There's a class called System.Web.UI.PageParser that has a handy static method called GetCompiledPageInstance(), which returns an object that implements IHttpHandler, and more specifically, an instance of our page. It's important to understand the distinction that you aren't transferring execution to a particular page, you're actually creating an instance of the page (it's an object, like anything else), and handing it off to ASP.NET so it can render it and send it to the client. The .NET documentation says you shouldn't use this class directly from your code, but I say throw caution to the wind and tell Microsoft you'll do as you please. The class is sealed, however, so you can't inherit from it. No matter, the method takes three parameters:

Setting up web.config

OK, so now we've got this class, and we've compiled it (or put it in our /App_code folder if we're using ASP.NET v2.0 or later). Now we need a little configuration action to map page requests to our fancy new handler factory. (Again, see the previous article on HttpHandlers if you need a refresher.) We'll need to add a line in the httpHandlers section of web.config like this:

<configuration>
   <system.web>
      <httpHandlers>
         <add verb="*" path="content/*.aspx" type="MyPageFactory" />
      </httpHandlers>
   </system.web>
</configuration>

The add line takes any request made to an .aspx page in the /content folder and sends the request to our MyPageFactory class. The way it's setup now, any request to /content/blah.aspx or /content/ugh.aspx will in fact be rerouted to /content.aspx! Even post-backs will occur to the page requested, even though it does not physically exist. Pretty cool, eh?

Making the page do something

Don't start patting yourself on the back just yet. Content.aspx is just going to serve up the same boring content no matter what, unless you give it something else to do, based on the request. Recall that in our MyPageFactory class we discovered the name of the request file, sans the file extension, and stuffed it in the Items collection. Now, in Content.aspx, we can do something with that information. Here's a page fragment from Content.aspx:

<%@ Page Language="C#" %>
<script runat="server">
   void Page_Load(object sender, EventArgs e)
   {
      TheLiteral.Text = HttpContext.Current.Items["fileName"];
   }
</script>
<html>
   <head runat="server">
      <title>Awesome Content</title>
   </head>
   <body>
      <form runat="server">
          <p>Request was for: <asp:Literal ID="TheLiteral" runat="server" /></p>
      </form>
   </body>
</html>

If you guessed that a request to /content/Blah.aspx would show a page that has the words, "Request was for: blah", then you understand everything about this article. Congratulations! If you don't quite get it, here's the play-by-play:

Taking it a step further

You've already got the makings of a content management application here. You could implement any kind of logic you want in the factory to choose which page to execute, making for a good templating system. In the pages, you could load a particular user control into a placeholder based on the name of the requested page (I frequently did this before master pages came along). The possibilities are endless.