HttpModules
Many of these events can be sunk in the Global.asax of an application. By doing this, however, you limit the functionality to that application. To sink these events in a more reusable fashion, create an HttpModule. By adding a single line to the machine.config, your HttpModule affects all applications on the machine; by adding instead a single line to the web.config file, your HttpModule affects just that one application. The line to load an HttpModule looks like the following:
<httpModules> <add type="SimpleModules.SimpleHttpModule, SimpleModules" name="SimpleHttpModule" /> </httpModules>
Let's take a look at a couple of sample HttpModules that handle some of the events on this class.
A Simple BeginRequest and EndRequest Module
BeginRequest is the first event to fire when processing a request. EndRequest is almost the last event to fire. Let's write an HttpModule that sinks these events and uses them to time stamp the output HTML with the time that the request began processing and when it finished processing. This information might be useful if you were trying to profile a group of pages.
We will create this as our first HttpModule. First, we need to create a class. This class will implement the IHttpModule interface. To implement this interface, we need to supply two members: Init and Dispose. When ASP.NET loads our HttpModule to participate in processing a request, a reference to the HttpApplication object is passed to the Init method. We will then save a reference to this in a member variable for use later in our module. We will also wire up several event handlers off the HttpApplication object.
After we have implemented IHttpModule, we can get into doing the things that are specific to our task. In this example, we need to create event handlers for BeginRequest and EndRequest. We do this by first creating our functions like this:
public void BeginRequest(object sender, EventArgs e)
Next we need to wire them up in the Init method that is part of the IhttpModule interface like this:
application.BeginRequest += new System.EventHandler(BeginRequest);
Inside of BeginRequest and EndRequest, we will utilize a saved reference for HttpApplication to write into the output stream a comment tag containing the date and time. The complete HttpModule is shown in Listing 8.1.
Code Listing 8.1Implementation of a Module That Stamps the Begin and End Times into the Page
using System; using System.Web; namespace SimpleModules { /// <summary> /// Summary description for BeginEnd. /// <add type="SimpleModules.BeginEnd, SimpleModules" name="BeginEnd" /> /// </summary> public class BeginEnd : IHttpModule { private HttpApplication mApplication; public void Init(System.Web.HttpApplication application) { // Wire up beginrequest application.BeginRequest += new System.EventHandler(BeginRequest); // Wire up endrequest application.EndRequest += new System.EventHandler(EndRequest); // Save the application mApplication = application; } public void BeginRequest(object sender, EventArgs e) { mApplication.Response.Write("<!-- Begin Request Time: " + DateTime.Now.ToString("HH:mm:ss.fffffff") + " -->"); } public void EndRequest(object sender, EventArgs e) { mApplication.Response.Write("<!-- End Request Time: " + DateTime.Now.ToString("HH:mm:ss.fffffff") + " -->"); } public void Dispose() { } } }
To get this module to execute for a single application, we need to place it into the /bin directory and modify the web.config to include it in the httpModules section. The web.config should look like Listing 8.2.
Listing 8.2The web.config to Load the BeginEnd HttpModule
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <httpModules> <add type="SimpleModules.BeginEnd, SimpleModules" name="BeginEnd" /> </httpModules> </system.web> </configuration>
Now if we fire off a page in this application root, we will see the time stamps introduced as comments into the HTML. A sample page output is shown in Listing 8.3.
Listing 8.3The View Source of a Page That Has Been Affected by the BeginEnd Module
<!-- Begin Request Time: 19:02:04.1024016 --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Begin End</title> </head> <body MS_POSITIONING="GridLayout"> <form name="Form1" method="post" action="WebForm1.aspx" id="Form1"> <input type="hidden" name="__VIEWSTATE" value="dDwxNDEzNDIyOTIxOzs+" /> Time: 8/23/2001 7:02:04 PM </form> </body> </html> <!-- End Request Time: 19:02:04.4729344 -->
Right now, the module works with a single application. Move it to machine.config and register the assembly in the global assembly cache, and every ASP.NET page on the entire server would suddenly get these time stamps! This is clearly an incredibly powerful technique.
Filtering Output
The preceding example showed how to insert content into the output using Response.Write(). What if you want to filter the content in the page? Perhaps you are writing an advertising system that needs to be able to find certain tags in a page and replace them with an advertisement. Although this is a common type of task, this task is a bit tougher to do. No property on the response object allows you to retrieve the contents of a page and modify it in situ. If you think about how ASP.NET sends pages to the client, however, you can understand why this is so. Depending on the buffering state and the programmer's use of Response.Flush(), the entire page may never exist on the server. Instead, it may be streamed to the client in dribs and drabs. However, ASP.NET by default enables buffering, so it certainly would have been nice to give us access to that buffer. Perhaps in v.Next (the next version) the object model will be updated to allow this access.
So how do you get the page output? As it turns out, you don't get ityou filter it. It is possible to put a filter in place that inserts itself between ASP.NET and the client. As ASP.NET streams data back to the user, your "filter" can alter it. This filtering is done using the base classes in the .NET framework. .NET provides an abstract class called a Stream. The Stream class is used as a pattern for writing to memory, files, and even sockets. It should come as no surprise then that ASP.NET gives you access to the stream that is connected to the client via the Response.Filter property.
To filter the page output, create an object that derives from Stream and pass it the Response.Filter property. Then set the Response.Filter property to this object. Now when ASP.NET sends page output to the client, it is actually sending the page output to your object. You can then modify the content as you see fit, and when done with it, you write it to the client stream that was passed to your constructor.
This is easier to show than describe, so let's take a look at some code. Listing 8.4 shows the complete source for the ad insertion filter AdInserter.cs. Like the previous example, we implement IHttpModule. The difference is that in the BeginRequest event handler, we create an instance of the AdStream object, passing it the Response.Filter, which contains a stream pointed at the user. We then take the stream object and set the Response.Filter property to it.
Now the interesting work is actually done in AdStream. This is our "filter." Everything up to the Write() method toward the bottom is just implementing the required stream members. The Write() method is where things get interesting. ASP.NET calls Write() when it wants to send data to the client. The data is passed into Write() as a byte array. Byte arrays are great if we want to inspect things character by character, but in this case we are more interested in dealing in strings so that we can do some pattern matching. To convert the byte array to a string, use the UTF8Encoding class. This class converts a byte array to a Unicode string using UTF8 encoding. The result string is placed into a StringBuilder so that we can do simple replacement operations on it.
Strings are immutable, so simple string concatenations behind the scenes are really creating and destroying the underlying strings, causing a performance drain. The StringBuilder is a much more efficient way to do operations on a string. In this case, we are looking for the <adinsert></adinsert> tags, but this is a simplified task just for this example. In real life, you should instead search for <adinsert> only, do a string scan to find the </adinsert>, and thenbased on positionreplace what is between them. For simplicity, here we are replacing the exact match in this sample with a string that's derived by taking a random entry from the astrAds array. In a real ad insertion engine, this step would also be more complicated, most likely entailing a selection algorithm against a cache of items from a backing database store. Finally, the resulting string is written to the client stream using a StreamWriter, which supports writing a string to a stream without first having to convert it to a byte array.
Listing 8.4A Simple Ad Insertion Engine That Replaces <adinsert> Tags with an Ad
public class AdInserter : System.Web.IHttpModule { private System.Web.HttpApplication mApplication; public AdInserter() { } public void Init(System.Web.HttpApplication application) { // Wire up beginrequest application.BeginRequest += new System.EventHandler(BeginRequest); // Save the application mApplication = application; } public void BeginRequest(Object sender, EventArgs e) { // Create a new filter AdStream mStreamFilter = new AdStream(mApplication.Response.Filter); // Insert it onto the page mApplication.Response.Filter = mStreamFilter; } // AdStream filter public class AdStream : System.IO.Stream { // The ads to insert string[] astrAds = new string[] {"<adinsert><img src=\"deep_468x60a.gif\"></adinsert>", "<adinsert><img src=\"deepASPNET_color.gif\"></adinsert>"}; // The stream to the client private System.IO.Stream moStream; // Used to track properties not supported by the client stream private long mlLength; private long mlPosition; // An easy way to write a stream to the client private System.IO.StreamWriter mSR; public AdStream(System.IO.Stream stream) { // Save of the stream back to the client moStream = stream; // Create a streamwriter for later use mSR = new System.IO.StreamWriter(moStream); } public override bool CanRead { get { return false; } } public override bool CanSeek { get { return true; } } public override bool CanWrite { get { return true; } } public override long Length { get { return mlLength; } } public override long Position { get { return mlPosition; } set { mlPosition = value; } } public override int Read(Byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override long Seek(long offset, System.IO.SeekOrigin direction) { return moStream.Seek(offset, direction); } public override void SetLength(long length) { mlLength = length; } public override void Close() { moStream.Close(); } public override void Flush() { moStream.Flush(); } public override void Write(byte[] buffer, int offset, int count) { System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding(); // Get the string into a stringbuilder System.Text.StringBuilder strBuff = new System.Text.StringBuilder(utf8.GetString(buffer)); // Random number used to grab ads Random oRandom = new Random(DateTime.Now.Millisecond); int iRandom = oRandom.Next(astrAds.GetLowerBound(0), astrAds.GetUpperBound(0)); // Go through and find the <adinsert></adinsert> tags strBuff.Replace("<adinsert></adinsert>", astrAds[iRandom]); // Write to the stream mSR.Write(strBuff.ToString()); } } public void Dispose() { } }
The end result is a page that contains text from one of the elements in astrAds. Listing 8.5 shows the resulting HTML.
Listing 8.5The Output from AdInserter.cs to a Page with <adinsert> Tags
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title></title> <meta name="GENERATOR" content="Microsoft Visual Studio.NET 7.0"> <meta name="CODE_LANGUAGE" content="Visual Basic 7.0"> <meta name="vs_defaultClientScript" content="JavaScript"> <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5"> </head> <body MS_POSITIONING="GridLayout"> <adinsert><img src="deep_468x60a.gif"></adinsert><br> <form name="Form1" method="post" action="WebForm1.aspx" id="Form1"> <input type="hidden" name="__VIEWSTATE" value="dDwtMTI3OTMzNDM4NDs7Pg==" /> Time: 8/23/2001 8:27:12 PM</form> </body> </html>
Note the <img> tag that was inserted by the filter between the <adinsert> tags.
Forking the Filter
Filters work great if the task at hand calls for modifying the content as it streams to the client. Some tasks, however, don't fit this model. Suppose that you want to create something similar to the OutputCache in ASP.NET. For this to work, you need to have the entire contents of the page available after it has been written to the client. You might be thinking, "No problem, the stream has a read method." As it turns out, HttpResponseStream, which is the stream that ASP.NET uses to respond to a request, doesn't support the read operation. If you attempt to use it, you will get an UnsupportedException thrown. To make this idea work, your stream implementation must "Fork" the data written to it. One copy will be written to the client stream. The other copy will be written to an in-memory buffer that can then be read from at a later time. This way, when request processing is over, we can still access the content of the page.
The next example implements a very simplistic caching mechanism. It has an internal hash table that it uses to store pages that are keyed on the request URL. This example also uses two new events: ResolveRequestCache and UpdateRequestCache. You may wonder why two new events are needed. ResolveRequestCache is the appropriate event in this case because BeginRequest happens prior to the authentication and authorization stages. If you checked the cache for a page before those events fired, you could potentially return a cached page to an unauthorized user. That clearly would be undesirable. UpdateRequestCache is to place an executed page into the cache when it is done executing. Listing 8.6 contains the implementation of SimpleCache.
Listing 8.6Implementation of SimpleCache, an Output-Caching Mechanism
using System; using System.Web; using System.Collections; namespace SimpleModules { /// <summary> /// Summary description for SimpleCache. /// </summary> public class SimpleCache : IHttpModule { // The stored application private HttpApplication mApplication; // Hash to store cached pages Hashtable mHash = new Hashtable(); public void Init(HttpApplication app) { // Store off the application object mApplication = app; // Wire up our event handlers mApplication.ResolveRequestCache += new System.EventHandler(this.ResolveRequestCache); mApplication.UpdateRequestCache += new System.EventHandler(this.UpdateRequestCache); } public void Dispose() { } private void ResolveRequestCache(object sender, System.EventArgs e) { // is the url in the cache? if(mHash.Contains(mApplication.Request.Url)) { // Write it back from the cache mApplication.Response.Write(mHash[mApplication.Request.Url].ToString()); // Finish the request mApplication.CompleteRequest(); } else { // Create a new filter CacheStream mStreamFilter = new CacheStream(mApplication.Response.Filter); // Insert it onto the page mApplication.Response.Filter = mStreamFilter; // Save a reference to the filter in the request context so we can grab it in UpdateRequestCache mApplication.Context.Items.Add("mStreamFilter", mStreamFilter); } } private void UpdateRequestCache(object sender, System.EventArgs e) { if(!mHash.Contains(mApplication.Request.Url)) { // Grab the CacheStream out of the context CacheStream mStreamFilter = (CacheStream) mApplication.Context.Items["mStreamFilter"]; // Remove the reference to the filter mApplication.Context.Items.Remove("mStreamFilter"); // Create a buffer byte[] bBuffer = new byte[mStreamFilter.Length]; // Rewind the stream mStreamFilter.Position = 0; // Get the bytes mStreamFilter.Read(bBuffer, 0, (int)mStreamFilter.Length); // Convert to a string System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding(); System.Text.StringBuilder strBuff = new System.Text.StringBuilder(utf8.GetString(bBuffer)); // Insert the cached timestamp strBuff.Insert(0, "<!-- Cached: " + DateTime.Now.ToString("r") + " -->"); // Save it away mHash.Add(mApplication.Request.Url, strBuff.ToString()); } } public class CacheStream : System.IO.Stream { private System.IO.MemoryStream moMemoryStream = new System.IO.MemoryStream(); private System.IO.Stream moStream; public CacheStream(System.IO.Stream stream) { moStream = stream; } public override bool CanRead { get { return true; } } public override bool CanWrite { get { return true; } } public override bool CanSeek { get { return true; } } public override long Length { get { return moMemoryStream.Length; } } public override long Position { get { return moMemoryStream.Position; } set { moMemoryStream.Position = value; } } public override int Read(byte[] buffer, int offset, int count) { return moMemoryStream.Read(buffer, offset, count); } public override long Seek(long offset, System.IO.SeekOrigin direction) { return moMemoryStream.Seek(offset, direction); } public override void SetLength(long length) { moMemoryStream.SetLength(length); } public override void Close() { moStream.Close(); } public override void Flush() { moStream.Flush(); } public override void Write(byte[] buffer, int offset, int count) { moStream.Write(buffer, offset, count); moMemoryStream.Write(buffer, offset, count); } } } }
The pattern should be familiar by now. First, implement IHttpModule and save off a copy of the application. ResolveRequestCache is where things start to diverge from prior examples. In ResolveRequestCache, look in mHash to see if a cached copy of the page already exists. Call the Contains method, passing the URL of the request to determine if it is in the cache. If it is, retrieve the string from mHash, Response.Write it to the client, and then call HttpApplication.CompleteRequest. This call short-circuits execution of the request and causes ASP.NET to bypass the rest of the steps and stream the result back to the client. If the page is not in the cache, place an instance of CacheStream into Response.Filter, and also place a reference to CacheStream into HttpContext.Items. This reference is needed because the Response.Filter property always returns the stream that points to the client, even after it's set to point to a different stream. That way, multiple filters can be inserted and each can act on the stream. In this case, however, we need to get access to CacheStream later on during the UpdateRequestCache event.
To facilitate communication between events in HttpModules and/or HttpModules themselves, the HttpContext provides the items collection that allows data to be associated with the request. In this case, use it to store a reference to CacheStream. CacheStream inherits Stream and acts as the forking filter. Everything that is written to CacheStream is also written to an internal MemoryStream. CacheStream, unlike the previous examples, supports the Read method. When Read is called, information from the internal MemoryStream is returned. When UpdateRequestCache finally fires, it checks again to see if the current request is already in mHash. If it isn't, grab the CacheStream from the HttpContext and retrieve the copy of the page data that it contains. Add a comment to the beginning of the page data that stamps it with the date and time that the page was cached. This page is then placed into mHash, keyed off the URL. That's it! The OutputCacheModule in real life, of course, does considerably more than this, including aging of items from the cache and varying by parameters, but this HttpModule effectively demonstrates how to use the Filter property to get at the content of the page.
An Error Module
One of the coolest new application events in ASP.NET is the Error event. As mentioned before with an HttpModule you can sink this event in an application-specific way in Global.asax. You can redirect the user away from the error page to some other part of the site that is more appropriate than just an error message. It might be interesting to sink the error event in a module, however, to provide a non-application-specific piece of functionality. A common idea is to log the error to the event log for later analysis or perhaps even to e-mail it to the Web master. This can be done in an application-independent way, which indicates the need for an HttpModule.
Listing 8.7 shows an HttpModule that logs the error information to an event log and e-mails the Webmaster with the error information. It first attempts to connect to an event log called "ASP.NET ErrorModule," which is created if it doesn't already exist. Next, it gathers the error information from the HttpApplication.Context.Error property. This property returns the exception that was thrown during the processing of this request. Several of the Exception properties are bundled into a string, which is then logged to the event log. Finally, the error is sent to the Webmaster using the SmtpMailClass.
Listing 8.7An HttpModule That Handles Errors in an Application by Writing Them to the Event Log and E-mailing the Webmaster
public class ErrorModule : IHttpModule { private const string strEVENTLOGNAME = "ASP.NET ErrorModule"; private HttpApplication mApplication; public void Init(HttpApplication application) { // Save off the application mApplication = application; // Wire up the error handler mApplication.Error += new System.EventHandler(this.ErrorHandler); } private void ErrorHandler(object sender, EventArgs e) { // Create the event source if it doesn't exist if(!EventLog.SourceExists(strEVENTLOGNAME)) EventLog.CreateEventSource(strEVENTLOGNAME, strEVENTLOGNAME + " Log"); // Create an event log instance and point it at the event source EventLog el = new EventLog(); el.Source = strEVENTLOGNAME; // Create the error text string strErrorMessage = "An uncaught exception was thrown in your application\r\nUrl: " + mApplication.Request.Url.ToString() + "\r\nMessage:" + mApplication.Context.Error.Message + "\r\nStack Trace:" + mApplication.Context.Error.StackTrace; // Write the event log entry el.WriteEntry(strErrorMessage, EventLogEntryType.Error); // Mail the message to the web master System.Web.Mail.SmtpMail.Send("webserver@vergentsoftware.com", "ckinsman@vergentsoftware.com", "Web Site Error", strErrorMessage); } public void Dispose() { } }
This code results in the Event Log entry shown in Figure 8.1 and the e-mail message shown in Figure 8.2.
Figure 8.1 The resulting event log entry.
Figure 8.2 The resulting e-mail.
Notice that this HttpModule doesn't actually do any redirection. That is expected to be handled in an application-specific way. The sample shows another event handler defined in Global.asax for the Error event. This event handler is responsible for redirecting the user to a friendly error page. Both error event handlers fire during the processing of the request. This fact is important because it points out that multiple HttpModules can each be handling the same events. Listing 8.8 show the global.asax.
Listing 8.8Global.asax Does the Actual Redirection in an Application-Specific Way
public class Global : System.Web.HttpApplication { private void InitializeComponent() { this.Error += new System.EventHandler(this.Application_Error); } protected void Application_Error(object sender, EventArgs e) { Response.Redirect("friendlyerror.htm"); } }
Typically you wouldn't just redirect to another page in the event handler. You would normally do something along the lines of logging the error or notifying the administrator. If you just want to show another page instead of the error also check out the <CustomErrors> element of the web.config which is discussed in Chapter 5, "Configuration and Deployment".
Raising Events from an HttpModule
As mentioned previously, HttpModules are intended to be generic, containing no application logic. In many cases, however, you may want the developer of the application to tie application-specific code to your HttpModule. One way to do this is to raise events as part of your processing that the developer can sink in Global.asax to provide application-specific processing. Several of the built-in HttpModules raise events of this nature.
The way this is done is a little odd. No explicit event wireup is done. Instead, events are wired based on a naming convention. If you have a public event delegate in your code of the form:
public event EventHandler MyEvent
You can then put an event handler in the global.asax in the form friendlymodulename_eventname. When ASP.NET loads your HttpModule, it will dynamically wire up the event in the module to the event handler in the global.asax for you, based on the matching signatures. Listing 8.9 shows an HttpModule that raises an event in the global.asax of the application. It defines MyEvent and then raises it as part of the processing of BeginRequest.
Listing 8.9An HttpHandler That Raises an Event in Global.asax
public class EventRaise : IHttpModule { private HttpApplication mApplication; public event EventHandler MyEvent; public void Init(HttpApplication application) { // Save off the application object mApplication = application; // Wire up begin request mApplication.BeginRequest += new System.EventHandler(this.BeginRequest); } private void BeginRequest(object sender, EventArgs e) { OnMyEvent(new EventArgs()); } private void OnMyEvent(EventArgs e) { if(MyEvent!=null) MyEvent(this, e); } public void Dispose() { } }
Listing 8.10 shows the global.asax handling the event, based on the friendly name of the HttpModule, EventRaise, and the name of the event, OnMyEvent.
Listing 8.10The global.asax That Sinks the OnMyEvent Event from the HttpModule
public class Global : System.Web.HttpApplication { protected void EventRaise_MyEvent(object sender, EventArgs e) { Response.Write("MyEventFired!"); } }
Authentication Modules
In the previous chapter, we wrote an AuthenticateRequest handler that was used to do role mapping based on Forms Authentication with a custom ticket. None of the code in AuthenticateRequest was really application specific. It could easily be abstracted into an HttpModule that sinks the AuthenticateRequest event and can then be reused in many other applications. Converting this code to work in an HttpModule is straightforward. Listing 8.11 shows AuthModule, an implementation of the functionality from the DbFormUrl Listing 7.16 in Chapter 7.
Listing 8.11AuthenticateRequest in a Web Module for the DbFormUrl Example in Chapter 7
using System; using System.Collections; using System.ComponentModel; using System.Web; using System.Web.SessionState; namespace DBFormURL { /// <summary> /// Summary description for Global. /// </summary> public class Global : System.Web.HttpApplication { protected void Application_Start(Object sender, EventArgs e) { Application["DSN"] = "SERVER=localhost;UID=sa;PWD=;DATABASE=SecuritySample"; } protected void Application_AuthenticateRequest(object sender, EventArgs e) { // Make sure the user has been authenticated // This event fires for unauthenticated users also if(Request.IsAuthenticated) { // Get the users identity System.Web.Security.FormsIdentity fiUser = (System.Web.Security.FormsIdentity)User.Identity; // Get the ticket System.Web.Security.FormsAuthenticationTicket at = fiUser.Ticket; // Grab out the roles string strRoles = at.UserData; // Renew the ticket if need be System.Web.Security.FormsAuthenticationTicket ticket = System.Web.Security.FormsAuthentication.RenewTicketIfOld(at); if(ticket!=at) { // Get the encrypted version of the ticket string strEncrypted = System.Web.Security.FormsAuthentication.Encrypt(ticket); // Put it into a cookie HttpCookie hc = new HttpCookie(System.Web.Security.FormsAuthentication.FormsCookieName, strEncrypted); hc.Expires = DateTime.Now.AddMinutes(20); // Add it to the cookies collection Response.Cookies.Add(hc); } // Create a new principal which includes our role information from the cookie HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(fiUser, strRoles.Split(',')); } } } }
Rewriting Paths
Occasionally, a technique that you come up with for conveying information in a URL doesn't fit the standard model for URLs. A great example of this is the cookieless session management that we looked at in Chapter 4, "State Management and Caching." The URLs used in cookieless session management take on the following form:
http://localhost/sessionid/default.aspx
Where the sessionid part varies on a user-by-user basis. This is an invalid URL in the normal context of ASP.NET, so how is it handled? Behind the scenes, the cookieless session state HttpModule uses a method of the HttpApplication, RewritePath(). RewritePath allows you to take an incoming URL and change it to point to a different page. This is not a redirect, which requires a round trip to the client. It is also not a Server.Transfer; it happens prior to the PageHandlerFactory executing any code in a page.
RewritePath allows the cookieless session state HttpModule to change the preceding URL that ASP.NET looks for to the following:
http://localhost/default.aspx
The URL in the user's browser remains unchanged there's no noticeable difference. Let's take a look at an HttpModule that does something of this sort. The RewritePath module in Listing 8.12 sinks the BeginRequest event. Inside this event, it rewrites any request that is received by ASP.NET to instead point to the webform1.aspx file that is in the application root.
Listing 8.12RewritePath Module That Changes Every Request to Map to webform1.aspx
public class RewritePath : IHttpModule { private HttpApplication mApplication; public void Init(HttpApplication application) { // Save off the application mApplication = application; // Wire up the begin request mApplication.BeginRequest += new EventHandler(this.RewritePathHandler); } private void RewritePathHandler(object sender, EventArgs e) { mApplication.Context.RewritePath(mApplication.Request.ApplicationPath + "/webform1.aspx"); } public void Dispose() { } }
It doesn't matter what you type in as a URL, because as long as it ends in .aspx, you will see the content of webform1.aspx in the browser, even though the typed-in URL is persisted in the address bar of your browser. Figure 8.3 shows an attempt to browse to a fictitious URL and the resulting page that shows WebForm1.aspx.
Figure 8.3 The result of typing in a fictitious URL with the RewritePath module in place.