Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Solution for URL Rewriting in ASP.NET

0.00/5 (No votes)
3 Jul 2007 1  
Powerful solution for URL rewriting and handling rewritten parameters in ASP.NET.

Introduction

Screenshot - fig1.jpg

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:

  1. The URL Configuration Manager
  2. 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.

  3. The Navigation Manager
  4. 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.

  5. Postbacks
  6. 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:

  1. No need for any configuration changes to IIS or the server
  2. Easily defined rules with Regular Expressions
  3. Support for exclusions based on files, folders, and Regular Expressions
  4. Redirection facility
  5. 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:

<?xml version="1.0" encoding="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
    'Ensure that the configuration path exists
    If Not IO.File.Exists(ConfigurationPath) Then
      Throw New Exception("Could not obtain configuration information")
    End If

    'Load the configuration settings
    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
  
  'TODO: Insert class code here

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 the rewriting is disabled then return the source url
  If CType(configurationNode.Attributes("enabled").Value, Boolean) = False Then
    Return requestedPage
  End If

  'Iterate though the configuration document looking for a match
  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

    'Remove the domain and querystring information
    If HttpContext.Current.Request.RawUrl.Contains("?") Then
      CurrentUrl = HttpContext.Current.Request.RawUrl.Substring _ 
     (0, CurrentUrl.IndexOf("?"))
    End If

    'Add the querystring values to the dictionary
     processQuerystring()
  End Sub

  Private Sub processQuerystring()
    'Convert the querstring items to an array
    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()
    'Create an array to contain the raw information regarding the Url
    Dim items() As String = CurrentUrl.Split(New Char() {"/"c},  _ 
    StringSplitOptions.RemoveEmptyEntries)

    'Load the XML navigation structure
    'Ensure that the configuration path exists
    If Not IO.File.Exists(ConfigurationPath) Then
       Throw New Exception("Could not obtain configuration information")
    End If

    'Load the configuration settings
    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)
     'Iterate though the items, build up the navigation keys
     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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here