Rewriting the URL using IHttpHandlerFactory
posted by
Jeff
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:
context - The HttpContext of the current request. This is where you can
get all of the objects and data about the request that you're used to getting
(like Request, for example).
requestType - As in HTTP request types like GET or POST.
url - Not surprsingly, the URL of the request, like /SomeFolder/SomePage.aspx.
pathTranslated - Where the request physically maps to on the
server, like c:\intepub\wwwroot\SomeFolder\SomePage.aspx. This is
not particularly useful for us in this article.
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.
Not familiar with Items? It's a per-request cache where you can store stuff
and retrieve it later on in the request/response lifecycle. For more information
on this lifecycle, check out
Maximizing ASP.NET by, well, me, published
by Addison-Wesley, 2005.
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:
virtualPath - The path of the request, which is for our
purposes exactly like the url parameter passed into this method.
inputFile - Here's where we'll make magic, and map our
request to the Content.aspx file, sitting in the root of our
application.
context - This too comes right out of the method 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:
web.config is set up to forward all requests to .aspx
pages in our /content folder to the MyPageFactory
class.
- The class implements
IHttpHandlerFactory, and in that class
we figure out what file is being requested, and after stuffing that info into
Items, we get an instance of our Content.aspx
page.
Content.aspx checks the Items collection for the
data we put there, and does something to the content of the page based on that
data.
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.
©2013, POP World Media, LLC. All rights reserved
Legal, privacy, terms of service