4.5 Custom Modules


The last and perhaps most powerful point of extensibility in the HTTP pipeline is the module. Modules live at the application scope and can tap into any of the HttpApplication events. They are created the first time the application is created and exist throughout the lifetime of the application. Modules are typically used to perform pre- or postprocessing on requests , similar in many ways to ISAPI filters in IIS. ASP.NET itself uses modules for a number of application-level features, including authentication, authorization, output caching, and out-of-process session state management. Table 4-4 shows the various system-provided modules and the services they provide.

Table 4-4. Modules Defined in ASP.NET

Module

Purpose

OutputCacheModule

Page-level output caching

SessionStateModule

Out-of-process session state management

WindowsAuthenticationModule

Client authentication using integrated Windows authentication

FormsAuthenticationModule

Client authentication using cookie-based forms authentication

PassportAuthenticationModule

Client authentication using MS Passport

UrlAuthorizationModule

Client authorization based on requested URL

FileAuthorizationModule

Client authorization based on requested file

Note that all these services require hooks into the pipeline before the request is issued to a handler. This is a common requirement of a module, to be able to tap into the request processing pipeline and potentially alter, abort, or otherwise change the current request.

To construct your own module, you begin by building a class that implements the IHttpModule interface, shown in Listing 4-18. This interface consists of two methods , Init() and Dispose() . The Init method is called when the module is first created, and it takes as a parameter a reference to the current HttpApplication object. Typically, a module uses the application class to register delegates to some of the events exposed by HttpApplication . The Dispose method is called when the application is being closed, giving modules an opportunity to perform any cleanup necessary.

Listing 4-18 IHttpModule Interface
 public interface IHttpModule {   void Dispose();   void Init(HttpApplication context); } 

To see an example, let's build a module that tracks the request processing time for all requests serviced by an application. The logic for this class is identical to our earlier implementation of tracking request time through the global.asax file, and in fact, the capabilities of global.asax and modules largely overlap. We discuss how best to choose which technique to use in the next section. Listing 4-19 shows our TimerModule class, which will keep track of the start request time in the Items collection of the HttpContext class, as we did in our global.asax example. This time, for variety, instead of printing the timing results to the response buffer, we create a custom header that contains the timing information. [11]

[11] Thanks go to Aaron Skonnard, who first showed me this timer module example.

Listing 4-19 Sample Module to Collect Request Timing Information
 // File: TimerModule.cs // public class TimerModule : IHttpModule {   public void Dispose() {}   public void Init(HttpApplication httpApp)   {     // subscribe delegates to the BeginRequest and     // EndRequest events of the HttpApplication class     httpApp.BeginRequest +=             new EventHandler(this.OnBeginRequest);     httpApp.EndRequest +=             new EventHandler(this.OnEndRequest);   }   public void OnBeginRequest(object o, EventArgs ea)   {     HttpApplication httpApp = o as HttpApplication;     // record time that event was handled in     // per-request Items collection     httpApp.Context.Items["sTime"] = DateTime.Now;   }   public void OnEndRequest(object o, EventArgs ea)   {     HttpApplication httpApp = o as HttpApplication;     DateTime dt = (DateTime)httpApp.Context.Items["sTime"];     // measure time between BeginRequest event     // and current event     TimeSpan ts = DateTime.Now - dt;     httpApp.Context.Response.AddHeader("RequestTiming",                                        ts.ToString());   } } 

Now, to deploy this module, the class must be compiled into an assembly, deployed in the /bin directory of the application (or in the GAC), and an entry must be made in the web.config file of the application under the httpModules element. Listing 4-20 shows a sample web.config file that would register this module, assuming we compiled the class into an assembly named TimerModule . Note that the add element takes a name and a type for attributes, where the name is some application-unique string identifying the module, and the type is the class name (fully namespace qualified) followed by the assembly name .

Listing 4-20 Timer Module Configuration
 <! File: web.config > <configuration>   <system.web>     <httpModules>       <add name="Timer"            type="TimerModule, TimerModule" />     </httpModules>   </system.web> </configuration> 

4.5.1 Modules as Filters

In addition to monitoring requests and supplementing responses, modules can be used to filter both requests and responses. Filtering is useful if you want to modify the incoming request before it reaches its handler or to change the outgoing response as it is being written. Filters can be useful for adding things like common footers to all pages processed by an application or even stripping off portions of a response before sending it back to the client.

To build a module that acts as a filter, you need to construct a new Stream -derived class and at the beginning of each request ( Application_BeginRequest ), create a new instance of your Stream -derived class and assign it to the Filter property of either the Request or the Response object. From this point on, all writes to the Response stream (or reads from the Request stream) will use your new stream class. The tricky part in getting a filter working properly is that you need to keep the old Request or Response stream around, cached in your custom stream, so that you can read from or write to the actual request or response stream when you need to. Your new stream class thus acts as a filter sitting between the original stream and any reads or writes. This relationship is shown in Figure 4-4.

Figure 4-4. Filtering with HttpModules

graphics/04fig04.gif

To see an example of building a filtering module, suppose we wanted to alter the outgoing response of all requests made to an application by stripping off some portion of the response and logging it to a file. As we will see in the next chapter, there is a compelling reason to build such a filtering module. One of the diagnostic features of ASP.NET, called tracing, augments the output of any given page with supplemental information about the request. This is extremely useful information, but there is no way to tell ASP.NET to write that trace information to a file instead of directly to the Response stream. So for our example, we will build an HttpModule that monitors all responses, looking for the beginning of the trace output of a response (if there is one). Once the trace output is detected , we will stop passing it along to the Response stream and instead redirect the output to a file stream to store on disk.

Our first task is to build the custom stream class that sits between any calls to Response.Write() and the actual Response stream. This class, which we call TraceRedirectStream , is shown in Listing 4-21. The first thing to note about this custom stream class is that it encapsulates two streams as data members . The first, called _primaryStream , is initialized to the actual Response stream. The second, called _otherStream , is initialized to a file stream to which the trace information is to be written. The constructor of our custom stream class takes the two streams as parameters, as well as the request URL so that it can document what request generated the trace information. Many of the Stream functions of this class simply forward along to the _primaryStream data member, so that it behaves like the standard Response stream in most ways. The interesting functions are Write and FindTraceStartTag . The purpose of FindTraceStartTag is to scan the text being written for the string that indicates the beginning of the trace output. This function is then used within the Write method to determine which stream to write to. The first time the trace start tag is encountered , we write out a header to the file stream to document the request URL, and then all subsequent writes to our stream are sent to the file stream and not to the original Response stream.

Listing 4-21 Custom Stream Class to Redirect Trace Output
 public class TraceRedirectStream : Stream {   private Stream  _primaryStream;   private Stream  _otherStream;   private bool    _inTrace = false;   private string  _requestUrl;   // Signals the start of the trace information in a page   private const string _cStartTraceStringTag =                         "<div id=\"__asptrace\">";   public TraceRedirectStream(Stream primaryStream,                              Stream otherStream,                              string requestUrl)   {     _primaryStream = primaryStream;     _otherStream   = otherStream;     _requestUrl    = requestUrl;   }   private void WriteLine(Stream s, string format,                          params object[] args)   {     string text = string.Format(format +                                 Environment.NewLine, args);     byte[] textBytes = Encoding.ASCII.GetBytes(text);     s.Write(textBytes, 0, textBytes.Length);   }   public override bool CanRead   {     get { return(_primaryStream.CanRead); }   }   public override bool CanSeek   {     get { return(_primaryStream.CanSeek); }   }   public override bool CanWrite   {     get { return(_primaryStream.CanWrite); }   }   public override long Length   {     get { return(_primaryStream.Length); }   }   public override long Position   {     get { return(_primaryStream.Position); }     set     {       _primaryStream.Position = value;       _otherStream.Position = value;     }   }   public override long Seek(long offset,                             SeekOrigin direction)   {     return _primaryStream.Seek(offset, direction);   }   public override void SetLength(long length)   {     _primaryStream.SetLength(length);   }   public override void Close()   {     _primaryStream.Close();     _otherStream.Close();   }   public override void Flush()   {     _primaryStream.Flush();     _otherStream.Flush();   }   public override int Read(byte[] buffer, int offset,                             int count)   {     return _primaryStream.Read(buffer, offset, count);   }   public override void Write(byte[] buffer, int offset,                               int count)   {     if (_inTrace)     {       // if we are writing out trace information,       // it is always the last part of the output stream,       // so just continue writing to the log file until       // the request completes.       _otherStream.Write(buffer, offset, count);     }     else     {       // We are not currently writing out trace information,       // so as we write response information, look for the       // trace start string, and begin writing to the trace       // log if we encounter it. Scan the entire buffer       // looking for the trace start tag.       int idx = FindTraceStartTag(buffer, offset, count);       if (idx > 0) // if non-negative, start tag found       {         WriteLine(_otherStream,                   "<hr/><h3>Request URL: {0}</h3>",                   _requestUrl);         _inTrace = true;         // write non-trace portion of buffer to primary         // response stream         _primaryStream.Write(buffer, offset, idx);         // write trace portion to other stream (log)         _otherStream.Write(buffer, idx+offset, count - idx);       }     }   }   public override int ReadByte()   {     int b = _primaryStream.ReadByte();     _otherStream.Position = _primaryStream.Position;     return(b);   }   public override void WriteByte(byte b)   {     if (this._inTrace)       _otherStream.WriteByte(b);     else       _primaryStream.WriteByte(b);   }   private int FindTraceStartTag(byte[] buffer, int offset,                                 int count)   {     int bufIdx = offset;     int ret = -1;     while ((bufIdx < count+offset) && (ret < 0))     {       if (buffer[bufIdx] ==                TraceRedirectStream._cStartTraceStringTag[0])       {         int i=1;         while ((i <            TraceRedirectStream._cStartTraceStringTag.Length)                && (bufIdx+i < count+offset))         {           if (buffer[bufIdx+i] !=                TraceRedirectStream._cStartTraceStringTag[i])             break;           i++;         }         if (i >=            TraceRedirectStream._cStartTraceStringTag.Length)           ret = bufIdx;       } // if (buffer[bufIdx]...       bufIdx++;     } // while (bufIdx < ...     return ret;   } // private int FindTraceStartTag... } // public class TraceRedirectStream... 

Now we are left with the task of installing this stream in place from a module. Our custom HttpModule adds handlers for the BeginRequest and EndRequest events so that our custom stream can be installed and closed when the request is complete. The installation of the stream involves creating a new instance and assigning it to the Filter property of the Response object with each new request. This custom module is shown in Listing 4-22.

Listing 4-22 TraceDumpModule Class
 public class TraceDumpModule : IHttpModule {   private TraceRedirectStream  _responseStream;   private string               _logFileName;   public void Init(HttpApplication httpApp)   {     _logFileName = @"C:\temp\tracelog.htm";     httpApp.BeginRequest +=             new EventHandler(OnBeginRequest);     httpApp.EndRequest += new EventHandler(OnEndRequest);   }   void OnBeginRequest(object sender, EventArgs a)   {     HttpApplication httpApp = sender as HttpApplication;     FileInfo fiLogFile;     if(File.Exists(_logFileName))       fiLogFile = new FileInfo(_logFileName);     // Open the log file (for appending) and log     // any trace output made to that request.     //     Stream responseLog = File.Open(_logFileName,                        FileMode.Append, FileAccess.Write);     long pos = httpApp.Request.InputStream.Position;     CopyStream(httpApp.Request.InputStream, responseLog);     httpApp.Request.InputStream.Position = pos;     // Set the response filter to refer to the trace     // redirect stream bound to the original response     // stream and the log file. As this stream processes     // the response data, it will selectively send non-     // trace output to the original stream, and trace     //output to the log file     //     _responseStream =            new TraceRedirectStream(httpApp.Response.Filter,                responseLog, httpApp.Request.Url.ToString());     httpApp.Response.Filter = _responseStream;   }   void OnEndRequest(object sender, EventArgs a)   {     if(_responseStream != null)       _responseStream.Close();   }   void CopyStream(Stream inStream, Stream outStream)   {     byte[] buf = new byte[128];     int bytesRead = 0;     while ((bytesRead=inStream.Read(buf, 0, buf.Length))             > 0)     {       outStream.Write(buf, 0, bytesRead);     }   }   public void Dispose()   {} } 

4.5.2 Module Pooling

Unlike handlers, which are pooled only when you create custom handlers whose IsReusable property returns true , modules are always pooled. Each time a new instance of the HttpApplication -derived class is created, a new set of modules is created to accompany it. Thus, the advice given earlier for application classes holds true for custom modules as well ”save no state between requests.

4.5.3 Modules versus global.asax

As noted earlier, all the functionality of a module can also be implemented in the application-specific global.asax file. The converse is not true, however, because global.asax provides several additional events and features that are not available to modules, such as the session start event or the ability to use the object tag to declare application or session scoped objects. For many tasks at the application level, however, it is not always clear whether the feature should be implemented in a module or in the global.asax file. Table 4-5 provides a comparison of various features supported by modules and the global.asax file.

Table 4-5. Module versus global.asax

Feature

Module

global.asax

Can receive event notifications for all HttpApplication -generated events

Yes

Yes

Can receive event notifications for Session_Start / _End , Application_Start / _End

No

Yes

Can be deployed at the machine level

Yes

No

Supports declarative object instantiation

No

Yes

Note that the one significant advantage of modules is that they can be deployed at the machine level. By deploying your module's assembly in the GAC, you can add that module either directly to the machine-wide machine.config file or individually to multiple applications' web.config files, without having to recopy the assembly to each application's /bin directory. The one significant advantage of the global.asax file is that it supports additional events issued when the application starts and ends and when each session starts and ends. The global.asax file also supports declarative object instantiation, but as we have seen, this is a feature most developers are unlikely to make much use of anyway.

As a general guideline, it is usually wise to place any application-specific features that are unlikely to be useful in other applications in global.asax . If you find yourself building a feature, such as a response timer, that might be useful to more than one application, consider building it as a module instead of coding it directly into global.asax .



Essential ASP.NET With Examples in C#
Essential ASP.NET With Examples in C#
ISBN: 0201760401
EAN: 2147483647
Year: 2003
Pages: 94
Authors: Fritz Onion

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net