Introduction
This solution provides a facility for ASP.NET developers to rewrite URLs used in their applications in an extensible and manageable way. The solution is built into three sections:
- The URL Configuration Manager
Allows configuration of the rewriting rules. This has been developed to allow for rewriting as well as the exclusion of certain files or folders and redirection.
- The Navigation Manager
This allows the developer to manage the virtual structure of the website. The examples provided work from an XML file but could easily be changed to work from another type of data source. This module is, I believe, something which sets this example of URL rewriting apart from most of the other samples available. It allows folders to be defined as parameters which can then be requested by the developer.
- Postbacks
One of the problems which I found with most of the rewriting solutions available is that they post back to the re-written page, making the URLs inconsistent. By overriding the base HTMLForm, we can avoid that.
Background
URL rewriting is a method allowing the URL of the page to be displayed to the user in a friendly way whilst also providing the required information to the developer. There are currently many solutions available, ranging from the built-in ASP.NET rewriting to ISAPI filters for IIS.
Why this Solution?
There are many ways of implementing rewriting in ASP.NET, but I have found very few examples which allow for the functionality which I have needed in my applications which prompted me to develop this article. This solution features:
- No need for any configuration changes to IIS or the server
- Easily defined rules with Regular Expressions
- Support for exclusions based on files, folders, and Regular Expressions
- Redirection facility
- Easy access to the parameters provided
One of the best features of this is that there is no need to consider the 'real' page; each page is rewritten, and the navigation functions provide access to all aspects of both the URL and the querystring.
1. The URL Configuration Manager
The task of this module is to manage the rules associated with URL rewriting. It is based on an external XML file in this example, but could easily be incorporated into the web.config file if preferred.
A sample configuration file would be:
="1.0" ="utf-8"
<urlconfiguration enabled="true" />
<excludepath url="~/testfolder/(.*)" />
<excludefile url="~/testpage.aspx" />
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
<rewrite url="~/(.*).aspx" newpath="~/default.aspx" />
</urlconfiguration />
This sample shows the possible types of rules which are available. These are described in greater detail below.
<excludepath url="~/testfolder/(.*)" />
This rule defines any request for a file contained in testfolder to not be rewritten. This would also include files in any child folders.
<excludefile url="~/testpage.aspx" />
This rule states that any request for the file testpage.aspx which sits in the application root will not be rewritten.
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
The redirect rule will rewrite any file matching the specified criteria to the URL defined in the newPath
attribute.
<redirect url="~/newfolder/(.*)" newpath="~/testpage.aspx" />
The redirect rule bypasses the rewriting, and causes an instant Response.Redirect
to the specified URL, which could be a page within or external to the site.
<rewrite url="~/(.*).aspx" newpath="~/default.aspx" />
This final rule will rewrite any file matching the criteria to be handled by a specific page. This could be changed easily to allow anything, for example, in the /books/ folder to be rewritten to showbook.aspx if needed.
Now we have a configuration file and we need to create a couple of classes: one to read the data, and the other, a class implementing IHTTPModule
, which will perform the redirection routine.
Firstly, we will create a class XMLConfigurationManager
which reads the XML from the specified configuration file to be made available to the redirection class.
Public Class XMLConfigurationManager
Private _configurationPath As String
Public Property ConfigurationPath() As String
Get
Return _configurationPath
End Get
Set(ByVal value As String)
_configurationPath = value
End Set
End Property
Public Sub New(ByVal configurationDocumentPath As String)
ConfigurationPath = configurationDocumentPath
End Sub
Friend Function GetConfiguration() As XmlNode
If Not IO.File.Exists(ConfigurationPath) Then
Throw New Exception("Could not obtain configuration information")
End If
Dim settings As New XmlDocument
settings.Load(ConfigurationPath)
Return settings.ChildNodes(1)
End Function
End Class
This class has a constructor which takes the path of the configuration file (which can be stored in the web.config file) and provides a function which will return the root configuration node.
The second class needed is the HTTPModule which does the rewriting; in this case, a class named UrlRewritingModule
.
Public Class UrlRewritingModule
Implements System.Web.IHttpModule
End Class
This creates the class as an HTTPModule. We can then add this to the web.config; I shall go into that further into the article.
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub
Public Sub Init(ByVal context As System.Web.HttpApplication)
Implements System.Web.IHttpModule.Init
If context Is Nothing Then
Throw New Exception("No context available")
End If
AddHandler context.AuthorizeRequest, AddressOf Me.rewrite
End Sub
The Init
method shown above simply ensures that the application exists, and creates a handler which will fire once the request is authorized.
Private Sub rewrite(ByVal sender As Object, ByVal e As EventArgs)
Dim app As HttpApplication = CType(sender, HttpApplication)
If app Is Nothing Then
Throw New Exception("No application available")
End If
Dim urlConfiguration As New XMLConfigurationManager _
(HttpContext.Current.Request.PhysicalApplicationPath + _
System.Configuration.ConfigurationManager.AppSettings("UrlDataRelativePath"))
Dim requestedPage As String = "~" + app.Request.RawUrl
Dim querystring As String = String.Empty
If requestedPage.Contains("?") Then
querystring = requestedPage.Substring(requestedPage.IndexOf("?") + 1)
requestedPage = requestedPage.Substring(0, requestedPage.IndexOf("?"))
End If
Dim newUrl As String = getRewrittenUrl(requestedPage,
urlConfiguration.GetConfiguration)
If Not String.IsNullOrEmpty(querystring) Then
newUrl += "?" + querystring
End If
HttpContext.Current.RewritePath(newUrl, False)
End Sub
This method starts by reading the configuration from the XMLConfigurationManager
class using the file path which is obtained from the web.config file. The requested URL is then split from the querystring whilst being passed to the getRewrittenUrl
function. Once done, the querystring is re-attached to the URL and the page is rewritten using the ASP.NET httpcontext.current.rewritepath
function.
The final part of the URL configuration is the function to return the rewritten URL based on the rules which have been provided.
Private Function getRewrittenUrl(ByVal requestedPage As String, _
ByVal configurationNode As XmlNode) As String
If CType(configurationNode.Attributes("enabled").Value, Boolean) = False Then
Return requestedPage
End If
For Each childNode As XmlNode In configurationNode.ChildNodes
Dim regEx As New Regex(childNode.Attributes("url").Value, _
RegexOptions.IgnoreCase)
If regEx.Match(requestedPage).Success Then
Select Case childNode.Name
Case "excludePath", "excludeFile"
Return requestedPage
Case "rewrite"
Return childNode.Attributes("newPath").Value
Case "redirect"
HttpContext.Current.Response.Redirect _
(childNode.Attributes("newPath").Value)
End Select
End If
Next
Return requestedPage
End Function
The above code iterates though the rules, and depending on the name of the node, will perform the required function which returns the original URL in the case of the excludes, redirecting to the specified page, or rewriting the URL to the designated file.
2. The Navigation Manager
The navigation manager is the functionality which sits on top of the URL rewriting and provides information regarding the current page to the developer. This module again works from an external XML file, but I would imagine in many cases, this would be better served from a database. For this reason, I have implemented a base class and an XML based manager; a SQL/OLEDB or other provider could easily be developed using similar methods.
Theory
The way in which the navigation is designed is to allow for fixed and 'dynamic' folder structures within the website. Any dynamic folder is one which can be exposed as a key-value pair to the application. An example of this is the URL /products/shoes/nike/shoes.aspx.
- /products/ - Fixed folder used to separate areas of functionality on the site.
- /shoes/ - The category perhaps. A dynamic value which the developer will need to be able to display information to the user.
- /nike/ - The manufacturer. Again a dynamic folder.
- /shoes.aspx - The page name.
What the navigation manager will allow is the ability to, in code, request the current category, manufacturer, or page being viewed; for example:
Dim currentManufacturer as string = provider.GetNavigationValue("manufacturer")
or
Dim pageName as string = provider.GetNavigationValue("CurrentPage")
The Configuration File
This file creates the virtual structure of the website navigation. The example has been converted over from my current solution in SQL to XML to aid make writing this article a little easier.
<?xml version="1.0" encoding="utf-8" ?>
<Navigation>
<Folder Name="Products" Value="products" IsDynamic="False">
<Folder Name="Category" Value="productCategory" IsDynamic ="True" />
</Folder>
<Folder Name="About" Value="about" IsDynamic="False" />
<Folder Name="Articles" Value="articles" IsDynamic="False">
<Folder Name="Subject" Value="articleSubject" IsDynamic="true">
<Folder Name="Year" Value="year" IsDynamic="true" />
</Folder>
</Folder>
</Navigation>
As you can see, this is a simple folder structure with definitions as to whether the folder is dynamic or static. One limitation is that each folder can only contain one dynamic folder; otherwise, the application would not be able to determine which key to use.
The Name
attribute specifies a more friendly name for the folder, possibly to use in breadcrumbs or display on the site. The Value
attribute is the key name which will be exposed to the developer when attempting to obtain the value, and IsDynamic
specifies whether the folder is dynamic (obvious).
NavigationProviderBase
This base class contains the basic logic for managing the navigation.
Public MustInherit Class NavigationProviderBase
Protected MustOverride Sub processNavigation()
Private _navigationParts As New Dictionary(Of String, String)
Friend Property NavigationParts() As Dictionary(Of String, String)
Get
Return _navigationParts
End Get
Set(ByVal value As Dictionary(Of String, String))
_navigationParts = value
End Set
End Property
Private _querystringParts As New Dictionary(Of String, Object)
Friend Property QuerystringParts() As Dictionary(Of String, Object)
Get
Return _querystringParts
End Get
Set(ByVal value As Dictionary(Of String, Object))
_querystringParts = value
End Set
End Property
Private _currentUrl As String
Public Property CurrentUrl() As String
Get
Return _currentUrl
End Get
Set(ByVal value As String)
_currentUrl = value
End Set
End Property
Public Sub New()
Dim querystring As String = String.Empty
CurrentUrl = HttpContext.Current.Request.RawUrl
If HttpContext.Current.Request.RawUrl.Contains("?") Then
CurrentUrl = HttpContext.Current.Request.RawUrl.Substring _
(0, CurrentUrl.IndexOf("?"))
End If
processQuerystring()
End Sub
Private Sub processQuerystring()
For Each key As String In HttpContext.Current.Request.QueryString.Keys
QuerystringParts.Add(key, HttpContext.Current.Request.QueryString(key))
Next
End Sub
End Class
The base class contains information for creating the querysting values, which can be made available to the page alongside the navigation values (which are interpreted in a different class to allow for different data sources). The implementation of the querystring into this class was done as a nice-to-have, but is in no way instrumental to the application.
The XMLNavigationProvider
This inherits from the NavigationProviderBase
(above), and has the job of reading data from the XML file into the key-value pair list available to the page.
Public Class XMLNavigationProvider
Inherits NavigationProviderBase
Private _configurationPath As String
Public ReadOnly Property ConfigurationPath() As String
Get
Return _configurationPath
End Get
End Property
Public Sub New(ByVal configurationPath As String)
MyBase.New()
_configurationPath = configurationPath
processNavigation()
End Sub
Protected Overrides Sub processNavigation()
Dim items() As String = CurrentUrl.Split(New Char() {"/"c}, _
StringSplitOptions.RemoveEmptyEntries)
If Not IO.File.Exists(ConfigurationPath) Then
Throw New Exception("Could not obtain configuration information")
End If
Dim settings As New XmlDocument
settings.Load(ConfigurationPath)
ProcessNode(items, settings.ChildNodes(1), 0)
End Sub
Protected Sub ProcessNode(ByVal items() As String, _
ByVal parentNode As XmlNode, ByVal currentLevel As Integer)
If currentLevel >= items.Length Then Return
If items(currentLevel).ToLower.Contains(".aspx") Then
NavigationParts.Add("currentPage", _
items(currentLevel).Replace(".aspx", ""))
End If
Dim currentFolder As XmlNode = Nothing
For Each item As XmlNode In parentNode.ChildNodes
If CType(item.Attributes("IsDynamic").Value, Boolean) _
And Not items(currentLevel).ToLower.Contains(".aspx") Then
currentFolder = item
Else
If CType(item.Attributes("Value").Value, String).ToLower _
= items(currentLevel).ToLower Then
currentFolder = item
End If
End If
Next
If currentFolder IsNot Nothing Then
NavigationParts.Add(CType(currentFolder.Attributes("Value").Value _
, String).ToLower, items(currentLevel))
ProcessNode(items, currentFolder, currentLevel + 1)
End If
End Sub
End Class
The code is mainly based around a main function which iterates though the navigation parts (essentially folders) and maps these values up to the keys which are contained in the configuration file. These are then added to the collection of NavigationParts
.
Web.config Changes
There are a few changes needed to the web.config file to enable URL rewriting. We need to define the paths to both the navigation and the URL configuration files, and also add the HTTPModule.
<appsettings />
<add value="/config/URLConfiguration.xml" key="UrlDataRelativePath" />
<add value="/config/NavigationConfiguration.xml" key="NavigationDataRelativePath" />
</appsettings />
And inside the system.web
node, we need:
<httpmodules />
<add name="UrlRewritingModule" type="Rewriting.UrlRewritingModule" />
</httpmodules />
Handling Postbacks
At this point, you will have a solution which correctly rewrites the URL, but when posting back, the application will return to the rewritten page, which is not desired functionality. To get around this, we can create a custom HTMLForm which ensures that the postback occurs to the rewritten page.
Public Class FormRewriterControlAdapter
Inherits System.Web.UI.Adapters.ControlAdapter
Protected Overrides Sub Render(ByVal writer As _
System.Web.UI.HtmlTextWriter)
MyBase.Render(New RewriteFormHtmlTextWriter(writer))
End Sub
End Class
Public Class RewriteFormHtmlTextWriter
Inherits HtmlTextWriter
Sub New(ByVal writer As HtmlTextWriter)
MyBase.New(writer)
Me.InnerWriter = writer.InnerWriter
End Sub
Sub New(ByVal writer As System.IO.TextWriter)
MyBase.New(writer)
MyBase.InnerWriter = writer
End Sub
Public Overrides Sub WriteAttribute(ByVal name As String, _
ByVal value As String, ByVal fEncode As Boolean)
If (name = "action") Then
Dim Context As HttpContext
Context = HttpContext.Current
If Context.Items("ActionAlreadyWritten") Is Nothing Then
value = Context.Request.RawUrl
Context.Items("ActionAlreadyWritten") = True
End If
End If
MyBase.WriteAttribute(name, value, fEncode)
End Sub
End Class
You can then implement this in your application by adding a Form.browser file to your App_Browsers folder containing the following text:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.HtmlControls.HtmlForm"
adapterType="URLRewriting.Rewriting.FormRewriterControlAdapter" />
</controlAdapters>
</browser>
</browsers>
Source
Please download the full solution linked above for the source code and a simple test application.
History
- 2 Jul 2007: After comments made by Andrei Rinea, I have updated the code so that it no longer uses an inherited form to allow the postbacks, which would break the design-time view, but now uses a customised
ControlAdapter
. I found this code on Scott Gu's blog, and have implemented it into this solution.
This solution has been designed to be as generic as possible, and in reality, may not provide all of the requirements you need for your application. I am more than happy to suggest any changes which may help you.