Today, I would like to show you how you can create a small, easy and flexible URL Rewriter / Redirector Module.
Introduction
In state of the art webs, a URL Rewriter is essential for a recognizable URL and for SEO (search engine optimization). Only this way, we can build dynamic / virtual URLs with which a user can guess the topic only by looking at the URL.
So a URL Rewriter just does nothing less than gives non-file-matching URLs to the user and internally rewrites the path to a server-known script which returns the data.
Let’s start with the modules.
In a normal ASP.NET web project, there are .aspx pages, perhaps .ashx HttpHandlers and in some cases, there is a global.asax to provide methods which handles a request.
We could start the URL-Rewriter by adding some code in the existing (or in a new) global.asax, but this would not give us the flexibility to use the URL-Rewriter module in another project. So we will start our module by creating a new class-library calling it “RedirectorModule” and adding two classes called “RedirectHandler” and “RewriteHandler” (used for the demo):
First we will prepare both files, that they can handle incoming requests. To achieve this, we have to add a reference to System.Web (perhaps you have to import a reference to the System.Web .Net library) and implement the IHttpModule Interface this way:
Then we have to attach a method to handle the requests to the HttpApplication.BeginRequest event:
public void Init(HttpApplication context) { context.BeginRequest += context_BeginRequest; }
And implement the method:
private void context_BeginRequest(object sender, EventArgs e) { }
So by attaching to the BeginRequest of the HttpApplication, our attached method will be executed every time a request will be processed by our module.
Now we can start implementing the Redirect and Rewrite functions of our two HttpModules.
Redirect-Module
First we start with the Redirector module. In this module I will show you how you can create a URL-dependent redirector, which you can use for example for short-url’s on your page (example: http://yourdomain.com/news will redirect to http://yourdomain.com/company/news/).
To achieve this, we have to add the following code to the context_BeginRequest() method.
We start with some initialization:
// Initialization. var application = (HttpApplication)sender; var context = application.Context; var request = context.Request; var response = context.Response;
then, we need the path and querystring parameters of the current request to decide where we have to redirect:
// Load the path and query parameters. var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture); var query = request.Url.Query; // Process the path parameter. int lastSlashIndex = path.LastIndexOf('/'); if (lastSlashIndex > 0) { path = path.Substring(0, lastSlashIndex); }
In this code, we first get the lower variant of the Url.AbsolutePath. Then the path will be cutoff at the last “/” that we only get the folder part of the path.
After we have the raw URL-path part, we can process it. In our example, we make a simple switch / case:
// Process the current URL. switch(path) { case "/news": response.Redirect("/company/news/"); break; }
This part is the main-processing of the URL. It can be done (like in our example) by a switch / case, with a database query over a redirect-table, with a regular expression search, or with anything else where you can decide what you want to do with the URL.
So the whole context_BeginRequest() looks like this:
private void context_BeginRequest(object sender, EventArgs e) { // Initialization. var application = (HttpApplication)sender; var context = application.Context; var request = context.Request; var response = context.Response; // Load the path and query parameters. var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture); var query = request.Url.Query; // Process the path parameter. int lastSlashIndex = path.LastIndexOf('/'); if (lastSlashIndex > 0) { path = path.Substring(0, lastSlashIndex); } // Process the current URL. switch(path) { case "/news": response.Redirect("/company/news/"); break; } }
Now we finished our test Redirect-Module and we can start implementing the Rewrite-Module.
Rewrite-Module
The Rewrite-Module is built mostly the same way as the Redirector-Module, so I will not repeat the equal code and just show you the starting “template”:
private void context_BeginRequest(object sender, EventArgs e) { // Initialization. var application = (HttpApplication)sender; var context = application.Context; var request = context.Request; var response = context.Response; // Load the path and query parameters. var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture); var query = request.Url.Query; // Process the path parameter. int lastSlashIndex = path.LastIndexOf('/'); if (lastSlashIndex > 0) { path = path.Substring(0, lastSlashIndex); } // Process the current URL. // ## PROCESS CODE ## }
Now the main difference between the redirect and the rewrite is, that the rewrite usualy does not break the request, while the redirect completely ends the request and sends a HttpResponse with the status code 302 (moved) back to the browser, which makes a new request to the new URL.
So in the rewrite module processing, we want to handle the request to rewrite the URL http://yourdomain.com/company/news/ to the internal URL http://yourdomain.com/default.aspx?category=company&page=news.
Here is the code to manage the rewrite to the internal URL:
var pathParts = path.Split(new []{ '/' }, StringSplitOptions.RemoveEmptyEntries); // process the path by the rewrite, if there is a path. if (pathParts.Length > 0) { // Define the rewrite path template. const string rewritePath = "~/Default.aspx?category={0}&page={1}"; // Load the category part (first part of the path). var category = pathParts[0]; // Load the page part (if there is more than one path part available). var page = pathParts.Length > 1 ? pathParts[1] : string.Empty; // Rewrite the URL according the rewrite path template (and do not rebase the // client path). context.RewritePath( String.Format(CultureInfo.CurrentCulture, rewritePath, category, page), false); }
This is done just by splitting the path into its parts and assume that the first part is the category and the second part is the page parameter.
Now that we only rewrite unknown URL’s (and that known pages still get accessed normally), we make a check around the above code, if the current URL exists in our file-system:
if (!File.Exists(context.Server.MapPath(request.FilePath)))
So our rewrite code overall looks like this:
private void context_BeginRequest(object sender, EventArgs e) { // Initialization. var application = (HttpApplication)sender; var context = application.Context; var request = context.Request; if (!File.Exists(context.Server.MapPath(request.FilePath))) { // Load the path and query parameters. var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture); var query = request.Url.Query; // Process the path parameter. int lastSlashIndex = path.LastIndexOf('/'); if (lastSlashIndex > 0) { path = path.Substring(0, lastSlashIndex); } // Process the current URL. var pathParts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); // process the path by the rewrite, if there is a path. if (pathParts.Length > 0) { // Define the rewrite path template. const string rewritePath = "~/Default.aspx?category={0}&page={1}"; // Load the category part (first part of the path). var category = pathParts[0]; // Load the page part (if there is more than one path part available). var page = pathParts.Length > 1 ? pathParts[1] : string.Empty; // Rewrite the URL according the rewrite path template (and do not rebase the client path). context.RewritePath( String.Format(CultureInfo.CurrentCulture, rewritePath, category, page), false); } } }
After the HttpContext.RewritePath(), the server calls the rewritten page on the server (without any feedback to the browser before the rewritten page gets back). So we are able to create virtual paths according any specification (database defined URL’s, Regex URL’s, key / value based URL’s etc).
Web.config modifications
At the end, we need to inform our web server (normally an IIS), that the URL processing must be done not by the standard ASP.NET HttpModule (which just returns physical files) but by our newly created Redirect- and Rewrite module. This requires some modifications in the site’s web.config file.
First, we have to add and enable the HttpModules in our web.config by adding this lines:
<configuration> <system.web> <httpModules> <clear/> <add name="RedirectHandler" type="RedirectorModule.RedirectHandler"/> <add name="RewriteHandler" type="RedirectorModule.RewriteHandler"/> </httpModules> </system.web> <system.webServer> <modules runAllManagedModulesForAllRequests="true"> <remove name="RedirectHandler"/> <add name="RedirectHandler" type="RedirectorModule.RedirectHandler"/> <remove name="RewriteHandler"/> <add name="RewriteHandler" type="RedirectorModule.RewriteHandler"/> </modules> </system.webServer> </configuration>
In the httpModules node of the configuration/system.web, first we need to clear the existing modules, then we have to add our own modules. There you have to specify the name of the HttpModule and the type which will handle the requests. Now because the redirects must be executed mostly before the rewrites, we have to first add the redirect module. After that, we add the rewrite module.
The second part of the web.config modifications, the system.webserver part, is only needed for IIS 7 or higher for ASP.NET 3.5 / 4.0 projects. The parameter “runAllManagedModulesForAllRequests” does just say the IIS, that the below stated modules just handle all requests (and not just the ASP.NET known file-requests).
Attention: On the IIS 6, there are modifications on the IIS configurations needed that the ASP.NET knows that all file-types should be handled by the ASP.NET parser. The Wildcard Application Mapping settings are described here: http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/5c5ae5e0-f4f9-44b0-a743-f4c3a5ff68ec.mspx?mfr=true
Finish, conclusion and known problems
Now we are finished with our redirect and rewrite module and we can start having fun with it :)
In my opinion, this is the best way to get custom URL’s which are highly customizable and real good for handling SEO problems!
Known problems
The rewrite is really cool, but there are some points where you have to look at. Especially you have to be careful by using forms, because the form-action URL will be completely messed up after a rewrite (if you use internal query-string parameters in the rewritten URL). This can be handled through a ControlAdapter for the form server control, where you just reset the action URL to an empty string at the rendering phase. This way, a post-back will always be handled by the current URL.
So I hope you enjoyed this HowTo! If you have any suggestions or any feedback (or found errors here), please feel free to write a comment! I really appreciate all comments!