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 give 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 handle 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 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-urls 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:
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:
var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture);
var query = request.Url.Query;
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
:
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)
{
var application = (HttpApplication)sender;
var context = application.Context;
var request = context.Request;
var response = context.Response;
var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture);
var query = request.Url.Query;
int lastSlashIndex = path.LastIndexOf('/');
if (lastSlashIndex > 0)
{
path = path.Substring(0, lastSlashIndex);
}
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)
{
var application = (HttpApplication)sender;
var context = application.Context;
var request = context.Request;
var response = context.Response;
var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture);
var query = request.Url.Query;
int lastSlashIndex = path.LastIndexOf('/');
if (lastSlashIndex > 0)
{
path = path.Substring(0, lastSlashIndex);
}
}
Now the main difference between the redirect
and the rewrite
is that the rewrite
usually 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);
if (pathParts.Length > 0)
{
const string rewritePath = "~/Default.aspx?category={0}&page={1}";
var category = pathParts[0];
var page = pathParts.Length > 1 ? pathParts[1] : string.Empty;
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 URLs (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)
{
var application = (HttpApplication)sender;
var context = application.Context;
var request = context.Request;
if (!File.Exists(context.Server.MapPath(request.FilePath)))
{
var path = request.Url.AbsolutePath.ToLower(CultureInfo.CurrentCulture);
var query = request.Url.Query;
int lastSlashIndex = path.LastIndexOf('/');
if (lastSlashIndex > 0)
{
path = path.Substring(0, lastSlashIndex);
}
var pathParts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length > 0)
{
const string rewritePath = "~/Default.aspx?category={0}&page={1}";
var category = pathParts[0];
var page = pathParts.Length > 1 ? pathParts[1] : string.Empty;
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 URLs, Regex URLs, key / value based URLs, 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 these 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.
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 URLs which are highly customizable and really 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!