Rewrite.NET — A URL Rewriting Engine for .NET

By Robert Chartier


This article examines how to take advantage of HttpModules to create a URL rewriting engine in .NET. This tool has been described as the Swiss Army Knife of URL manipulation. If you don’t have a fundamental understanding of what HttpModules are and how to create and use them, review Mansoor Ahmed Siddiqui’s article “HTTP Handlers and HTTP Modules in ASP.NET”.


Section 1: The Engine and a Simple Rule



This section shows how easy it is to create the rewrite engine as an HttpModule and to create a simple rule. The next section dives into creating a more complex rule. Read over Section 1 only to learn about the underlying implementation of the rules engine and its rules. To see how to add or modify the existing rules, skim through this section and then skip to Section 2.


Keep in mind that when referring to the “engine” itself, I’m referring to the portion of the application that is responsible for loading and executing the “rules”. “Rules” refers to those portions of the application that actually process the rules set by the administrator. Rules are any class that implements the “IRules” interface (see below).


Step 1: Creating the HttpModule


The first step in creating any HttpModule is creating the class that implements the IHttpModule interfaces. Start up Visual Studio .NET, create a new C# Web application (“Rewrite.Test”), and then add a new C# Class library named “Rewrite.NET” to the same solution. Create the new Web application first so that we can use that application to test the HttpModule and not break any other portion of the site.


I have renamed the default “Class1” to “Rewrite”. Figure 1.1 below is its full source. (Don’t forget to add the reference to System.Web in the class library).

Figure 1.1.1 Rewrite.cs Source Listing

using System;

 

namespace Rewrite.NET {

public class Rewrite : System.Web.IHttpModule {

 

/// <summary>

/// Init is required from
the IHttpModule interface

/// </summary>

/// <param name="Appl"></param>

public void

Init(System.Web.HttpApplication Appl) {

//make sure to wire up to BeginRequest

Appl.BeginRequest+=new System.EventHandler(Rewrite_BeginRequest);

}

 

/// <summary>

/// Dispose is required
from the IHttpModule interface

/// </summary>

public void Dispose()
{

//make sure you clean up after yourself

}

 

/// <summary>

/// To handle the starting
of the incoming request

/// </summary>

/// <param name="sender"></param>

/// <param name="args"></param>

public void
Rewrite_BeginRequest(object sender,
System.EventArgs args) {

//process rules here

}

 

}

}


There really isn’t anything new in this block of code. Note, however, that I have trapped only the BeginRequest within the Init() method. The Rewrite_BeginRequest is where we will implement our rules engine.


At this step, take time to make the necessary modifications to the Web.config file so ASP.NET will know about the new handler. Figure 1.1.2 below shows the changes made to the Web.config file in the “Rewrite.Test” application.


Figure 1.1.2 Web.config Changes

<system.web>

<httpModules>

<add type="Rewrite.NET.Rewrite,Rewrite.NET" name="Rewrite.NET" />

</httpModules>

</system.web>


Keep in mind that once you add the handler to Web.config, ASP.NET will require that the DLL actually be placed into that application’s /bin folder. To help with this, set the Output Path for the Rewrite.NET project’s properties to the “/bin” folder (C:InetpubwwwrootRewrite.Netbin). Now, all that remains is to rebuild the solution and refresh any page within that application.




Step 2: A Simple Rewrite Rule


In this section a very simple rule is created that allows the Web site administrator to easily add and remove rules from the rule set. The easiest rule simply maps one URL to another. Let’s say that you recently changed the location of some folders within your organization’s site and want all of the links to stay valid. Well, we can write a very simple rule to handle this. Here are some examples:





Old URLNew URL
foo.com/aboutus.htmlfoo.com/aboutus.aspx
foo.com/help/foo.com/docs/help/


Obviously, there are many instances when something this simple would come in handy, such as when mapping a file or directory request to another file or directory request, for example. In order to accomplish this, there are three tasks to complete:



  1. Create the configuration section in the Web.config file (add new items)
  2. Load the configuration file in at runtime
  3. Process request, and rewrite the URL (redirect the browser).


Creating the configuration section in the Web.config file is very easy to do. (To review the <configSections> element, review the documentation at

http://msdn.microsoft.com/library/en-us/cpgenref/html/gngrfconfigsectionselementcontainertag.asp,
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondeclaringcustomconfigurationsections.asp,
http://msdn.microsoft.com/library/en-us/cpguide/html/cpcondeclaringsectiongroups.asp.)


Here are the additions to the Web.config file:


Figure 1.2.1 Web.config Additions

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<configSections>

<sectionGroup name="Rewrite.NET">

<section name="SimpleSettings" type="System.Configuration.NameValueSectionHandler,System" />

</sectionGroup>

</configSections>

<Rewrite.NET>

<SimpleSettings>

<add key="/rewrite.net/webform1.aspx" value="/rewrite.net/finalpage.aspx" />

</SimpleSettings>

</Rewrite.NET>


And then the new Rewrite_BeginRequest event handler is captured:


Figure 1.2.2

public void
Rewrite_BeginRequest(object sender,
System.EventArgs args) {

//process rules here

 

//cast the sender to an
HttpApplication object

System.Web.HttpApplication Appl=(System.Web.HttpApplication)sender;

 

//load the settings in

System.Collections.Specialized.NameValueCollection
SimpleSettings =
(System.Collections.Specialized.NameValueCollection)System.Configuration.ConfigurationSettings.GetConfig("Rewrite.NET/SimpleSettings");

 

//see if we have a match

for(int

x=0;x<SimpleSettings.Count;x++) {

string source=SimpleSettings.GetKey(x);

string dest = SimpleSettings.Get(x);

if(Appl.Request.Path.ToLower() == source.ToLower()) {

SendToNewUrl(dest,
Appl);

break;

}

}

}

 

public void
SendToNewUrl(string url,
System.Web.HttpApplication Appl) {

Appl.Context.RewritePath(url);

}


Notice how we are simply scanning the new configuration section for the source URL, and if it matches, the request is redirected to the new URL that was also specified in the config file.





Step 3: Adding Extensibility


In this step we will add some flexibility to our solution by creating an extensible rules engine. Take time now to add a new C# Class library to this solution named “RulesEngine”. Within this new project, an interface, “IRules”, will be defined, which all of our rules need to implement. We then will implement the actual engine, which will process these rules based on that interface and in turn redirect the user to the appropriate destination. Finally we will modify our Rewrite_BeginRequest method to use the engine to add new rules and to execute them.


The Interface


Our interface is very simple. It only has one method, “Execute”, which will take the HttpApplication, the current path, and the settings section (from the Web.config file) as parameters. This will give each rule access to all of the HttpApplication’s members, including the current path (either actual path or some transformed path along with the query string). We are also specifying the settings section so that we can have more than one rule set using the same object. That is, we can specify more than one set of rules for the same rule definition (object that implements the IRules interface). For example, we wanted to use a simple rule, but still be able to apply different rules on later dates. This will be made clearer later. Figure 1.3.1 below has the complete listing.


Figure 1.3.1 IRules Interface Source Listing

using System;

 

namespace RulesEngine {

public interface IRules {

string Execute(System.Web.HttpApplication Appl, string Path, string

SettingsSection);

}

}


The Engine


The concept of a rules-engine based on an interface is not a new one. It merely will loop for each item in the list and call the Execute method. In this case, the goal was to be able to add rules on top of other rules, or execution of the rule processing will break when the app discovers any matching or duplicate rules.


For example, if you had to apply a set of rules to an application because of some path changes, and then you needed to apply more rules on top of those (since over time, things change), this method would be a good choice for a cumulative rule set. Alternatively, you could simply allow the application to break out of the rules loop, and redirect as soon as a path is found in any set of rules. Notice I have an enum declared for this within the engine. Figure 1.3.2 below is the listing for the rules engine:


Figure 1.3.2 The Rules Engine

public string
Execute() {

string r="";

string newresult="";

//tear off the keylist for easy
access

string[] keylist = new string[rules.Keys.Count];

rules.Keys.CopyTo(keylist,0);

RulesEngine.IRules rule=null;

if(rules!=null && rules.Count>0) {

//let s process the first off of the top, in case we only
have one item

rule =
(RulesEngine.IRules)rules[keylist[0]];

if(rule!=null) {

//execute it, and return if we need to

r =
rule.Execute(appl, Getpath(appl), (string)keylist[0]);

if(engineType==EngineTypes.BreakOnFirst &&

r!="")

return r;

else {

for(int x=1;x<rules.Count;x++) {

//each rule
will simply take the Application and modify the path

//it will
return the new path to be used for successive rules

//this allows
you to chain together successive rules

rule =
(RulesEngine.IRules)rules[keylist[x]];

//if r is
"" then let s set it back to the path again.

//each rule can
optionally use its value, but it is needed for the simple rule

//it is merely
used to carry forward the last transformation that was done to the path

if(r=="")
r=Getpath(appl);

newresult = rule.Execute(appl, r, (string)keylist[x]);

//should we
return because we want to break on the first item?

if(engineType==EngineTypes.BreakOnFirst
&& newresult!="") {

r
= newresult;

break;

}

//make sure r
has the most recent value

r=(newresult=="")?r:newresult;

}

}

} else {

return r;

}

}

return r;

}




Changes to Our Simple Rule


Our simple rule consists of the majority of the code that we placed into the Rewrite_BeginRequest method above, with the small exception of returning the new URL, if found, instead of automatically redirecting. How and when redirection will occur will be controlled by the engine. Figure 3.3 below is the first implementation of our IRules interface for the engine with the Simple Rule set.


Figure 1.3.3 Simple Rule

using System;

 

namespace RulesEngine {

/// <summary>

///
Summary description for SimpleRule.

/// </summary>

public class SimpleRule : IRules {

public string

Execute(System.Web.HttpApplication Appl, string
Path, string SettingsSection){

//load the settings in

System.Collections.Specialized.NameValueCollection
SimpleSettings = (System.Collections.Specialized.NameValueCollection)System.Configuration.ConfigurationSettings.GetConfig(SettingsSection);

 

//see if we have a match

for(int
x=0;x<SimpleSettings.Count;x++) {

string
source=SimpleSettings.GetKey(x);

string
dest = SimpleSettings.Get(x);

if(Path.ToLower()
== source.ToLower()) return dest;

}

return "";

}

}

}

Lastly we need to bring it all together. The Rewrite_BeginRequest method will need to be modified to use the engine and to execute the rules in order. Don’t forget to add the Project Reference to the new project in our solution. Figure 1.3.4 shows the new Rewrite_BeginRequest method and Figure 1.3.5 shows the relevant section of the Web.config file.

Figure 1.3.4 The New Rewrite_BeginRequest

public void
Rewrite_BeginRequest(object sender, System.EventArgs
args) {

//process rules here

 

//cast the sender to an
HttpApplication object

System.Web.HttpApplication
Appl=(System.Web.HttpApplication)sender;

 

RulesEngine.Engine e = new
RulesEngine.Engine();

RulesEngine.SimpleRule simple = new
RulesEngine.SimpleRule();

e.AddRule("Rewrite.NET/SimpleSettingsMay1", simple);

e.EngineType=RulesEngine.Engine.EngineTypes.Cumulative;

string r = e.Execute(Appl);

//only redirect if we have to

if(r!="" &&

r.ToLower()!=Appl.Request.Path.ToLower()) SendToNewUrl(r, Appl);




<configSections>

<sectionGroup name="Rewrite.NET">

<section name="SimpleSettingsMay1" type="System.Configuration.NameValueSectionHandler,System" />

<section name="SimpleSettingsMay10" type="System.Configuration.NameValueSectionHandler,System" />

</sectionGroup>

</configSections>

 

<Rewrite.NET>

<SimpleSettingsMay1>

<add key="/rewrite.net/webform1.aspx" value="/rewrite/index.aspx" />

</SimpleSettingsMay1>

</Rewrite.NET>


In the next section we will make one more final change to the Rewrite_BeginRequest method where we load the individual rules from the Web.config file. This will allow us to add on more rules by simply modifying the Web.config file and require no more recompiling of the actual HttpModule. We will only need to make the changes to the Web.config and restart the application iireset.exe.


Dynamic Rule Loading


Dynamic rule loading will allow our HttpModule to pull out the desired rules from the Web.config file at runtime. There are two important steps to accomplishing this task:



  1. Reading the new Index section in the Web.config
  2. Reading each item in the Index and loading the new rule


Remember that since the name for each section will be defined later, we will never know its actual name during design time. So in order to accommodate for this and to be able to indicate the assembly and class name each section will use, we will create a new section within our Rewrite.NET section group named “Index”. This new Index section will be used to list each section and the assembly information that each section will use. Keep in mind that each section is really a rule. We have a file (assembly), and within that file we are expecting a class which implements the IRules interface, so we need to specify the file and then the class. The assembly information provides simply the name of the DLL and the class to use in the DLL that implements our IRules interface.


Figure 1.3.6 lists the new and final Rewrite_BeginRequest method, Figure 1.3.7 lists the new Web.config sections, and Figure 1.3.8 is a listing of the new overloaded constructor for our engine where we load the individual rules that are found in the Web.config file.


Figure 1.3.6 Final Rewrite_BeginRequest

public void
Rewrite_BeginRequest(object sender,
System.EventArgs args) {

//process rules here

 

//cast the sender to an
HttpApplication object

System.Web.HttpApplication
Appl=(System.Web.HttpApplication)sender;

 

//load up the rules engine

RulesEngine.Engine e = new
RulesEngine.Engine(Appl);

string r = e.Execute();

 

//only redirect if we have to

if(r!="" &&
r.ToLower()!=RulesEngine.Engine.Getpath(Appl).ToLower()) SendToNewUrl(r, Appl);

 

 

}


Figure 1.3.7 New Web.config Details

<configSections>

<sectionGroup name="Rewrite.NET">

<!–index entry is
required–
>

<section name="Index" type="System.Configuration.NameValueSectionHandler,System" />

<!–each optional
section name needs to be defined for each rule/section you want–
>

<!–you can have more than
one section, with the same rule (SimpleRule, etc..)–
>

<section name="SimpleSettingsMay1" type="System.Configuration.NameValueSectionHandler,System" />

</sectionGroup>

</configSections>

 

<Rewrite.NET>

<Index>

<!

Format:

<add
key="SECTION NAME" value="NAMESPACE.CLASSNAME,ASSEMBLY
NAME" />

Example:

<add
key="SimpleSettingsMay1"
value="RulesEngine.SimpleRule,RulesEngine" />

>

<add key="SimpleSettingsMay1" value="RulesEngine.SimpleRule,RulesEngine" />

</Index>

<!–the
actual settings for the rule set for the section–
>

<SimpleSettingsMay1>

<add key="/rewrite.net/webform1.aspx" value="/someplace/" />

</SimpleSettingsMay1>

</Rewrite.NET>


Figure 1.3.8 Overloaded Engine Constructor

public
Engine(System.Web.HttpApplication Appl) {

appl=Appl;

//load up rules from web.config

System.Collections.Specialized.NameValueCollection
SectionIndex =

(System.Collections.Specialized.NameValueCollection)

System.Configuration.ConfigurationSettings.GetConfig("Rewrite.NET/Index");

 

if(SectionIndex!=null) {

for(int
x=0;x<SectionIndex.Count;x++) {

string sectionname = "Rewrite.NET/" +
SectionIndex.Keys[x];

string sectiondata = SectionIndex[x];

string[] asmdata = sectiondata.Split(‘,’);

if(asmdata.Length==2) {

string

progid = asmdata[0].Trim();

string
asmname = asmdata[1].Trim();

string
filePath = Appl.Request.PhysicalApplicationPath + @"bin" + asmname +

".dll";

if(progid
!="" && asmname!="" &&
System.IO.File.Exists(filePath)) {

System.Reflection.Assembly asm =
System.Reflection.Assembly.LoadFrom(filePath);

if(asm!=null) {

RulesEngine.IRules
rule = (RulesEngine.IRules)asm.CreateInstance(progid, true);

if(rule!=null) {

this.AddRule(sectionname, rule);

}

}

}

}

}

}

 

}


Once compiled, our HttpModule can handle dynamically added rules and rule sets for incoming requests.



Section 2: Adding More Rules



This step examines how to create a more complex rule using regular expressions and add it to the engine via the Web.config file.


In order to add new rules to our engine we simply need to create the new class. This class is derived from the IRules interface. For this section we will start with a completely new solution in VS .NET. We will add a new rule to our application to handle regular expression search and the replacing of the URL. Here are the steps to set up our environment for this or any other rule you wish to create:


  1. Load up a new C# Class Library named “RegExpRule” in VS .NET.
  2. Rename the class and the file name to “RegExp”.
  3. Right click the project, choose Properties, and set the “Output Path” to the same “bin” folder of the application where the HttpModule is installed. For example, mine is located at “C:InetpubwwwrootRewrite.NETbin”.
  4. Right click the project, Add Reference, add in the .NET reference to our “RulesEngine.dll”. Again, mine is located at “C:InetpubwwwrootRewrite.NETbinRulesEngine.dll”. Also add the reference to System.Web.
  5. Implement the IRules Interface for the RegExp class. See Figure 2.1.


Figure 2.1 The RegExp Class

using System;

 

namespace RexExpRule {

public class RegExp : RulesEngine.IRules {

public RegExp() {

}

public string
Execute(System.Web.HttpApplication Appl, string
Path, string SettingsSection){

return "";

}

}

}


The actual logic for our Regular Expression Rule now needs to be implemented. This rule will load up all of the regular expression strings out of the Web.config file and apply them to the path variable of the incoming request for matching and transforming the path. If you are unfamiliar with regular expressions, use the URLs in this article’s References section.


The key to this rule is .NET Framework’s System.Text.RegularExpressions.Regexp.Replace(String, String, String) Method (http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemTextRegularExpressionsRegexClassReplaceTopic6.asp). This method allows us to specify the input text (the path and query string), a pattern to match, and a replacement for that pattern. For example, if we had the path “/corp/home.aspx” and we wanted to redirect all of the incoming requests for the “/corp/” path to the new “/about/” folder, we could easily set up the parameters:


Input = “/about/home.aspx”
Pattern = “^/corp/(.*)”
Replacement = “/about/$1”

In order to do this, we simply add the Replace() method to our Execute() Method, as show in Figure 2.2 below.


Figure 2.2 The New Execute() Method

public string

Execute(System.Web.HttpApplication Appl, string
Path, string SettingsSection){

string input =
RulesEngine.Engine.Getpath(Appl);

 

System.Collections.Specialized.NameValueCollection Settings =
(System.Collections.Specialized.NameValueCollection)System.Configuration.ConfigurationSettings.GetConfig(SettingsSection);

 

//see if we have a match

for(int
x=0;x<Settings.Count;x++) {

string pattern=Settings.GetKey(x);

string replacement = Settings.Get(x);

input = System.Text.RegularExpressions.Regex.Replace(input,
pattern, replacement);

}

return input;

}


Next, we need to make a few additions to our Web.config in order to load and set up the actual rules conditions for our RegExp rule. As seen in the previous section we need to take a number of steps to accomplish this:


  1. Add a new <section> node within <sectionGroup> for the new rule

    <section name="RegExpRule1" type="System.Configuration.NameValueSectionHandler,System" />

  2. Add an entry into the <Index> node for the new rule

    <add key="RegExpRule1" value="RexExpRule.RegExp,RexExpRule" />

  3. And finally add our new RegExpRule1 section

    <RegExpRule1>

    <add key="^/rewrite.net/about/(.*)" value="/rewrite.net/corp/$1" />

    </RegExpRule1>



Now that it is done, notice we added the condition that when a URL matching “^/rewrite.net/corp/(.*)” is found (any file or folder under the corp directory of this application is found), we want to replace the URL with the pattern “/rewrite.net/about/$1”. We are basically shifting the entire request, query string and all, to the new “/about” path. This has its obvious advantages over our Simple Rule above because we do not need to specify a full path or query string to any file. Instead we gain the power of a more generic search-and-replace methodology.



Conclusion



In this article you’ve seen how easy it is to create any HttpModule for custom purposes. It’s exciting to see that Microsoft has given the average developer so much power, but we must always keep in mind that with this much power, it is easy to shoot ourselves in the foot. We must always use the right tool for the job.

We also covered the ability to leverage HttpModules in order to create a fairly simple URL Rewriter. That is, take the incoming request and, based on set rules, send the user to an alternative place.


I hope you enjoyed reading this article and implement some or all of its content in your real-life situations. If you have any questions, feel free to contact me at any time.



References



Extending ASP.NET with HttpHandlers and HttpModules – By Bipin Joshi

http://www.devx.com/dotnet/Article/6962/0/page/1


Apache HTTP Server Version 1.3 — Module mod_rewrite, a URL Rewriting Engine

http://httpd.apache.org/docs/mod/mod_rewrite.html


Apache 1.3 URL Rewriting Guide

http://httpd.apache.org/docs/misc/rewriteguide.html


<configSections> element at MSDN

http://msdn.microsoft.com/library/en-us/cpgenref/html/gngrfconfigsectionselementcontainertag.asp


Declaring and Accessing Custom Configuration Sections

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondeclaringcustomconfigurationsections.asp


Declaring and Accessing Section Groups

http://msdn.microsoft.com/library/en-us/cpguide/html/cpcondeclaringsectiongroups.asp


Regular Expressions Usage in C#

http://www.c-sharpcorner.com/3/RegExpPSD.asp


Regular Expression Library

http://regxlib.com/

.NET Framework Class Library’s Regex.Replace Method (String, String, String)

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemTextRegularExpressionsRegexClassReplaceTopic6.asp



About the Author



Robert Chartier has developed IT solutions for more than nine years with a diverse background in both software and hardware development. He is internationally recognized as an innovative thinker and leading IT architect with frequent speaking engagements showcasing his expertise. He’s been an integral part of many open forums on cutting-edge technology, including the .NET Framework and Web Services. His current position as vice president of technology for Santra Technology (http://www.santra.com) has allowed him to focus on innovation within the Web Services market space.


He uses expertise with many Microsoft technologies, including .NET, and a strong background in Oracle, BEA Systems, Inc.’s BEA WebLogic, IBM, Java 2 Platform Enterprise Edition (J2EE), and similar technologies to support his award-winning writing. He frequently publishes to many of the leading developer and industry support Web sites and publications. He has a bachelor’s degree in Computer Information Systems.


Robert Chartier can be reached at rob@santra.com.


More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read