|
Introduction
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" at http://www.15seconds.com/issue/020417.htm.
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:\Inetpub\wwwroot\Rewrite.Net\bin\). 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 URL | New URL |
| foo.com/aboutus.html | foo.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:
- Create the configuration section in the Web.config file (add new items)
- Load the configuration file in at runtime
- 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:
- Reading the new Index section in the Web.config
- 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) {
|