IN THIS CHAPTER
An ASP.NET application raises certain events at the application level. For example, an application event is raised whenever a new user requests a page, whenever an application shuts down, and whenever an unhandled exception occurs.
This chapter discusses two methods of handling application-wide events. You learn how to implement application event-handlers both by using the Global.asax file and by using custom HttpModules. In particular, in this chapter you will learn
How to automatically track errors in your application with the Global.asax file
How to rewrite requests for one page into requests for another page in the Global.asax file
How to implement a custom file cache with the Global.asax file
How to create a cookieless authentication module
How to log the performance of an application with a custom HttpModule
When you create a new ASP.NET Web application with Visual Studio .NET, a Global.asax file is automatically added to your project. Every ASP.NET Web application can have one, and only one, Global.asax file. This file must be located in the root directory of the application.
If you open the Global.asax file from the Solution Explorer window, you are presented with the file in Design view (see Figure 18.1). In Design view, you can add components, such as database components, by dragging the components from the Toolbox. If you double-click the Designer surface, you are switched to the Code Editor. Within the Code Editor, you can modify the existing list of event handlers or add new event handlers.
By default, the Global.asax file contains the following event handlers:
Application_Start
. Raised
when an application starts (for example, after rebooting the Web server or modifying the Global.asax file).
Session_Start
. Raised
when a new user requests the first page.
Application_BeginRequest
. Raised
whenever any page is requested.
Application_AuthenticateRequest
. Raised
after a user has been identified. Use this method to implement custom roles with Forms authentication (see Chapter 16, “Securing Your Application”).
Application_Error
. Raised
when any unhandled exception occurs in the application.
Session_End
. Raised
when a user session ends (by default, 20 minutes after a user has made the last page request).
The events in the Global.asax file also
apply to Web services. For example, whenever a Web service is requested, the Application_BeginRequest
method is executed.
There are additional event handlers that you can add to the Global.asax file. You can add handlers for any events raised by the HttpApplication
class. You also can use the Global.asax file to handle events raised by any custom HttpModules contained in your application.
Modifying the Global.asax file restarts the application, which blows away all data in the Cache and application and (in-process) session state. So, be careful when modifying this file in a production application.
One
of the most useful events that you can handle in the Global.asax file is the Error
event. This event is raised whenever any unhandled exception is raised in an application. In other words, this event is raised whenever there is any error in a page or component that is not handled otherwise.
You can use the Application_Error
method to record unhandled errors so that you can monitor the health of your Web application. For example, you can automatically email errors to yourself or record errors to a file.
To automatically email
yourself errors, use the following Application_Error
handler:
C#.
protected void Application_Error(Object sender, EventArgs e) { Exception objError; string strSubject, strMessage; // Create the Error Message objError = Server.GetLastError().GetBaseException(); strSubject = "Error in page " + Request.Path; strMessage = "Error Message: " + objError.Message + Environment.NewLine + "Stack Trace:" + Environment.NewLine + objError.StackTrace; // Send the Error Mail System.Web.Mail.SmtpMail.SmtpServer = "yourDomain.com"; System.Web.Mail.SmtpMail.Send("error@yourDomain.com", "[email protected]", strSubject , strMessage); }
VB.NET.
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs) Dim objError As Exception Dim strSubject, strMessage As String ' Create the Error Message objError = Server.GetLastError().GetBaseException() strSubject = "Error in page " & Request.Path strMessage = "Error Message: " & objError.Message & vbNewLine strMessage &= "Stack Trace: " & vbNewLine strMessage &= objError.StackTrace ' Send the Error Mail System.Web.Mail.SmtpMail.SmtpServer = "yourDomain.com" System.Web.Mail.SmtpMail.Send("error@yourDomain.com", "[email protected]", strSubject , strMessage) End Sub
This Application_Error
handler retrieves the last error with the help of the Server.GetLastError()
method. Notice that the GetBaseException()
method is used to retrieve the original exception that raised the current error.
The error message is created with the help of two properties of the Exception
class—the Message
and StackTrace
properties. The Message
property returns a human-readable description of the error, and the StackTrace
property returns the list of statements that occurred immediately before the error.
You’ll need to change any reference to yourDomain.com
in the previous code to the name of your domain. Also, you’ll want to change the email [email protected]
to your email address.
The Application_Error
method is executed
whenever there is an unhandled exception. This includes requests for non-existent pages and runtime errors. You can test the Application_Error
method by performing
the following steps:
Add a new Web Form Page to your project named CauseError.aspx
.
Switch to the Code Editor by double-clicking the Designer surface.
Enter the following code in the Page_Load
handler:
C#.
private void Page_Load(object sender, System.EventArgs e) { int zero = 0; Response.Write( 9/zero ); }
VB.NET.
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim Ka Ka.Boom() End Sub
Right-click the CauseError.aspx page in the Solution Explorer and select Build and Browse.
When the CauseError.aspx page opens, you’ll receive a runtime error. The Application_Error
method will email you the error message and stack trace for the
error.
The Application_BeginRequest
method
executes at the start of each and every page request. One way to take advantage of this method is by creating a page filter. When someone requests a certain page, you can cause another page to be automatically loaded.
The Application_BeginRequest
method is particularly valuable when used in conjunction with the Context.RewritePath
method. The Context.RewritePath
method rewrites one page request into another page request.
For example, one problem that you’ll encounter when maintaining a Web application is the problem of handling changes to the structure of your site. If you remove or rearrange the pages in an application and there are links to the original pages, users will encounter Page Not Found errors.
If you need to remove existing pages from an application, you can use the Application_BeginRequest
and Context.RewritePath
methods to rewrite requests for old pages to new pages. In other words, you can use the Application_BeginRequest
and Context.RewritePath
methods to hide changes to your application from the rest of the world.
Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.
Enter the following code for
the Application_BeginRequest
method:
C#.
protected void Application_BeginRequest(Object sender, EventArgs e) { switch (Request.Path.ToLower()) { case "/myapp/products.aspx": Context.RewritePath( "/myapp/default.aspx" ); break; case "/myapp/services.aspx": Context.RewritePath( "/myapp/default.aspx" ); break; } }
VB.NET.
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs) Select Case Request.Path.ToLower() Case "/myapp/products.aspx" Context.RewritePath("/myapp/default.aspx") Case "/myapp/services.aspx" Context.RewritePath("/myapp/default.aspx") End Select End Sub
Select Build Solution from the Build menu.
After you complete these steps, any requests for the /myApp/Products.aspx or /myApp/Services.aspx pages will be automatically rewritten as requests for the Default.aspx page.
The Session State module
exposes two events that you can handle in the Global.asax file with the Session_Start
and Session_End
methods. The Session_Start
method executes when a new user makes a page request. The Session_End
method executes when a user has not made a page request for more than 20 minutes.
For example, you can use the Session_Start
and Session_End
methods to load and save a shopping cart for a user. In the Session_Start
method, you can load the shopping cart from the database into Session State. In the Session_End
method, you can save the shopping cart from Session State back to the database.
Another way in which you can use the Session_Start
and Session_End
methods is for tracking the number of users currently using an application.
Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.
Enter the following code for the Session_Start
method:
C#.
protected void Session_Start(Object sender, EventArgs e) { Application.Lock(); Application[ "numUsers" ] = (int)Application[ "numUsers" ] + 1; Application.UnLock(); }
VB.NET.
Enter the following code for the Session_End
method:
C#.
protected void Session_End(Object sender, EventArgs e) { Application.Lock(); Application[ "numUsers" ] = (int)Application[ "numUsers" ] - 1; Application.UnLock(); }
VB.NET.
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs) Application.Lock() Application("numUsers") -= 1 Application.UnLock() End Sub
Enter the following code for the Application_Start
method:
C#.
protected void Application_Start(Object sender, EventArgs e) { Application[ "numUsers" ] = 0; }
VB.NET.
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) Application("numUsers") = 0 End Sub
Next, we need to create a page that displays the number of active sessions:
Add a new Web Form Page to your
project named TrackSessions.aspx
.
Add a Web Form Label
to the page.
Switch to the Code Editor by double-clicking the Designer surface.
Enter the following code for the Page_Load
handler:
C#.
private void Page_Load(object sender, System.EventArgs e) { Label1.Text = "There are " + Application[ "numUsers" ] + " active sessions"; }
VB.NET.
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Label1.Text = "There are " Label1.Text &= Application("numUsers") Label1.Text &= " active sessions" End Sub
Right-click the TrackSessions.aspx page in the Solution Explorer window and select Build and Browse.
When the TrackSessions.aspx page opens, it will display the number of active sessions (see Figure 18.2). When new users request a page from the application, the session count increases. Twenty minutes after a user leaves the application, the session count will decrease.
You can’t test the TrackSessions.aspx page by opening multiple instances of Internet Explorer on the same computer because all instances share the same session. If you open both Internet Explorer and Netscape, however, the TrackSessions.aspx should display two sessions because cookies are not shared across different types of browsers.
There are two
methods that you can add to the Global.asax file—Application_ResolveRequestCache
and Application_UpdateRequestCache
—that you can handle to implement custom page caching. For example, suppose that you want to cache every dynamic Web Form Page in an application to the hard drive. The first time anyone requests a particular Web Form Page, the generated output of the page is saved to a static file.
Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.
Enter the following code for the Application_ResolveRequestCache
handler:
C#.
protected void Application_ResolveRequestCache(Object sender, EventArgs e) { // Build path to static file string strCachedPage = System.IO.Path.GetFileNameWithoutExtension( Request.Path ) + ".cache"; // Only execute request when cached page not found if (System.IO.File.Exists( Server.MapPath( strCachedPage) )) { Response.WriteFile( strCachedPage ); this.CompleteRequest(); } }
VB.NET.
Sub Application_ResolveRequestCache(ByVal sender As Object, ByVal e As EventArgs) ' Build path to static file Dim strCachedPage As String = _ System.IO.Path.GetFileNameWithoutExtension(Request.Path) & ".cache" ' Only execute request when cached page not found If System.IO.File.Exists(Server.MapPath(strCachedPage)) Then Response.WriteFile(strCachedPage) Me.CompleteRequest() End If End Sub
Enter the following code for the Application_UpdateRequestCache
handler:
C#.
protected void Application_UpdateRequestCache(Object sender, EventArgs e) { // Build path to static file string strCachedPage = System.IO.Path.GetFileNameWithoutExtension( Request.Path ) + ".cache"; // Save page output to hard drive System.IO.StreamWriter objWriter; objWriter = System.IO.File.CreateText( Server.MapPath( strCachedPage) ); Server.Execute(Request.Path, objWriter); objWriter.Close(); }
VB.NET.
Sub Application_UpdateRequestCache(ByVal sender As Object, ByVal e As EventArgs) ' Build path to static file Dim strCachedPage As String = _ System.IO.Path.GetFileNameWithoutExtension(Request.Path) & ".cache" ' Save page output to hard drive Dim objWriter As System.IO.StreamWriter objWriter = System.IO.File.CreateText(Server.MapPath(strCachedPage)) Server.Execute(Request.Path, objWriter) objWriter.Close() End Sub
Select Build Solution from the Build menu.
The Application_ResolveRequestCache
method checks for a cached version of the page being requested on the hard drive. If the file exists, the method sends the contents of the static file to the output stream and prevents the original page that was requested from being processed.
For example, if you request a page named Products.aspx, the method looks for a file named Products.cache. If the file is found, the contents of this file are sent to the browser and the Products.aspx page is never processed.
The Application_UpdateRequestCache
method uses the Server.Execute
method to capture the rendered contents of the requested page and save it to the hard drive. The Application_UpdateRequestCache
method is executed for each page in an application only once—the first time a page is requested by
any user.
Unfortunately, there is no method in the framework for reading the response output stream. This means that we have to use a trick—the Server.Execute
method—to get the rendered content of a page.
An unfortunate implication of this trick is that a page is executed twice when it is cached. The page must be executed a second time by the Server.Execute
method to get the rendered content. Although this is bad, keep in mind that it only happens with the very first page request and never happens again.
There is one additional set of steps you must perform to get the custom caching to work. You must provide the ASPNET account with Write permissions on the folder that contains the pages being cached:
Right-click the folder containing the pages that you want to cache and select Properties.
Select the Security Tab.
Grant Write permissions to either the ASPNET or Guest account.
After you enable the custom file caching, every page in your application is dynamically generated only once. Thereafter, the output of the page is read directly from the hard drive. If you want to regenerate the content of a page, you need to delete the cached file on the hard drive that corresponds to it. For example, to regenerate the Products.aspx page, you would need to delete Products.cache.
An HttpModule is a .NET class that participates in each and every page request. You can implement the same type of functionality in an HttpModule as you can implement in the Global.asax file. Like the Global.asax file, an HttpModule can be used to handle application-wide events. However, unlike the Global.asax, an HttpModule must be explicitly compiled and registered in the Web.Config file before it can be used.
Behind the scenes, ASP.NET Session State, caching, and authentication are all implemented as HttpModules. If you don’t like the way that Microsoft implemented any particular one of these HttpModules, you can simply replace Microsoft’s HttpModule with one of your own.
You can create your own custom HttpModule by implementing the IHttpModule
interface. The IHttpModule
interface has the following two methods:
Init
. Used to initialize any resources and to wire-up any event handlers needed by the HttpModule
Dispose
. Used to release any resources used the HttpModule
In the following sections, we’ll create two modules—an authentication module and a performance logging module.
All three of the built-in authentication systems included in the .NET Framework are implemented with modules. In this section, we’ll create our own authentication module.
One problem with using the Forms authentication module included with the .NET Framework is that it assumes that users have cookies enabled on their browsers. Many organizations, especially government organizations, do not allow employees to have cookies enabled on their browsers for security reasons.
In this section, we’ll create a cookieless authentication module. Instead of using a cookie to identify a user, the module uses an authentication key passed in either a form or query string variable.
Because we don’t want usernames and passwords exposed in the browser address bar, we’ll use a hash algorithm to hide them. The authentication key is calculated by hashing a user’s combined username and password.
Additionally, we will store usernames and passwords in an XML file. We’ll cache the XML file in the authentication module for better performance. We’ll also create a file dependency on the XML file so that changes to the file will be immediately reflected in the module.
Let’s start by creating the cookieless authentication module itself:
Procedure 18.1. C# Steps
Add a new class to your project named CookielessAuthenticationModule.cs
.
Add the following code to the class:
using System; using System.Web; using System.Security.Principal; namespace myApp { public class CookielessAuthenticationModule : IHttpModule { public void Init(HttpApplication application) { application.AuthenticateRequest += new EventHandler( AuthenticateRequest ); } public void Dispose() {} private void AuthenticateRequest(Object sender, EventArgs e) { string authKey; HttpContext context = ((HttpApplication)sender).Context; // Calculate Login Url string urlLogin = context.Request.ApplicationPath + "/CookielessLogin.aspx"; urlLogin += "?ReturnUrl=" + context.Server.UrlEncode( context.Request.RawUrl ); if (context.Request.Path.ToLower().IndexOf( "cookielesslogin.aspx") != -1) return; // Check for authKey if (context.Request[ "authKey" ] == null) context.Response.Redirect( urlLogin ); authKey = context.Request[ "authKey" ]; // Validate authKey if (!CookielessAuthentication.ValidateKey( authKey )) context.Response.Redirect( urlLogin ); // Create the User string username = CookielessAuthentication.GetUsername( authKey ); GenericIdentity userIdentity = new GenericIdentity( username, "cookieless" ); context.User = new GenericPrincipal(userIdentity, null); } } }
Procedure 18.2. VB.NET Steps
Add a new class to your project named CookielessAuthenticationModule.vb
.
Add the following code to the class:
Imports System.Security.Principal Public Class CookielessAuthenticationModule Implements IHttpModule Public Sub Init(ByVal application As HttpApplication) Implements IHttpModule.Init AddHandler application.AuthenticateRequest, AddressOf AuthenticateRequest End Sub Public Sub Dispose() Implements IHttpModule.Dispose End Sub Private Sub AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs) Dim authKey As String Dim context As HttpContext Dim urlLogin As String Dim username As String Dim userIdentity As GenericIdentity context = sender.Context ' Calculate Login Url urlLogin = context.Request.ApplicationPath + "/CookielessLogin.aspx" urlLogin += "?ReturnUrl=" + context.Server.UrlEncode(context.Request.RawUrl) If context.Request.Path.ToLower().IndexOf("cookielesslogin.aspx") <> -1 Then Return End If ' Check for authKey If context.Request("authKey") = Nothing Then context.Response.Redirect(urlLogin) End If authKey = context.Request("authKey") ' Validate authKey If Not CookielessAuthentication.ValidateKey(authKey) Then context.Response.Redirect(urlLogin) End If ' Create the User username = CookielessAuthentication.GetUsername(authKey) userIdentity = New GenericIdentity(username, "cookieless") context.User = New GenericPrincipal(userIdentity, Nothing) End Sub End Class
The CookielessAuthentication
module contains the two methods required by the IHttpModule interface—Init
and Dispose
. The Init
method is used to wire the AuthenticateRequest
method to the application AuthenticateRequest
event. The Dispose
method is added but contains no content because we have no resources to clean up after the module exits.
All the work happens in the AuthenticateRequest
method. This method attempts to retrieve the authentication key from a query string or form variable. If the authentication key cannot be retrieved, the user is redirected to the CookielessLogin.aspx page.
The AuthenticateRequest
method calls a method named ValidateKey
to check whether the authentication key is valid. The ValidateKey
method is one method of the CookielessAuthentication
utility
class.
To create the CookielessAuthentication
utility class, do the following:
Procedure 18.3. C# Steps
Add a new class to your project named CookielessAuthentication.cs
.
Add the following code to the CookielessAuthentication
class:
using System; using System.Data; using System.Web; using System.Security.Principal; using System.Web.Caching; using System.Web.Security; namespace myApp { public class CookielessAuthentication { public static DataTable AuthKeys { get { HttpContext context = HttpContext.Current; DataSet dstKeys = (DataSet)context.Cache[ "Users" ]; if (dstKeys == null) { dstKeys = new DataSet(); dstKeys.ReadXml( context.Server.MapPath( "Users.xml" ) ); CalculateKeys( dstKeys ); context.Cache.Insert( "Users", dstKeys, new CacheDependency( context.Server.MapPath( "Users.xml" ))); } return dstKeys.Tables[0]; } } private static DataSet CalculateKeys(DataSet dstKeys) { dstKeys.Tables[0].Columns.Add("AuthKey"); foreach(DataRow drow in dstKeys.Tables[0].Rows) drow[ "AuthKey" ] = HashKey( (string)drow[ "name" ], (string)drow[ "password" ]); return dstKeys; } private static string HashKey(string Username, string Password) { return FormsAuthentication.HashPasswordForStoringInConfigFile( Username + Password, "md5" ); } public static bool ValidateKey(string Key) { string strMatch = "AuthKey='" + Key + "'"; DataRow[] arrMatches = AuthKeys.Select( strMatch ); if (arrMatches.Length == 0 ) return false; return true; } public static string GetUsername(string Key) { string strMatch = "AuthKey='" + Key + "'"; DataRow[] arrMatches = AuthKeys.Select( strMatch ); if (arrMatches.Length == 0 ) return String.Empty; return (string)arrMatches[0]["name"]; } public static bool Authenticate(string Username, string Password) { string strMatch = String.Format("name='{0}' AND password='{1}'", Username, Password); DataRow[] arrMatches = AuthKeys.Select( strMatch ); if (arrMatches.Length == 0 ) return false; // Create the User GenericIdentity userIdentity = new GenericIdentity( Username, "cookieless" ); HttpContext.Current.User = new GenericPrincipal(userIdentity, null); return true; } public static void RedirectFromLoginPage() { HttpContext context = HttpContext.Current; string urlReturn = context.Request[ "ReturnUrl" ]; if (urlReturn == null) urlReturn = context.Request.ApplicationPath + "/default.aspx"; urlReturn = AddAuthKey( urlReturn ); context.Response.Redirect( urlReturn ); } public static string AddAuthKey(string Url) { if (Url.IndexOf( "?" ) == -1 return String.Format( "{0}?authKey={1}", Url, GetAuthKey() ); else return String.Format( "{0}&authKey={1}", Url, GetAuthKey() ); } public static string GetAuthKey() { HttpContext context = HttpContext.Current; string username = context.User.Identity.Name; string strMatch = "name='" + username + "'"; DataRow[] arrMatches = AuthKeys.Select( strMatch ); if (arrMatches.Length == 0 ) return String.Empty; return (string)arrMatches[0]["AuthKey"]; } } }
Procedure 18.4. VB.NET Steps
Add a new class to your project named CookielessAuthentication.vb
.
Add the following
code to the CookielessAuthentication
class:
Imports System.Security.Principal Imports System.Web.Caching Imports System.Web.Security Public Class CookielessAuthentication Public Shared ReadOnly Property AuthKeys() As DataTable Get Dim context As HttpContext Dim dstKeys As DataSet context = HttpContext.Current dstKeys = context.Cache("Users") If dstKeys Is Nothing Then dstKeys = New DataSet() dstKeys.ReadXml(context.Server.MapPath("Users.xml")) CalculateKeys(dstKeys) context.Cache.Insert("Users", dstKeys, _ New CacheDependency(context.Server.MapPath("Users.xml"))) End If Return dstKeys.Tables(0) End Get End Property Private Shared Function CalculateKeys(ByVal dstKeys As DataSet) As DataSet Dim drow As DataRow dstKeys.Tables(0).Columns.Add("AuthKey") For Each drow In dstKeys.Tables(0).Rows drow("AuthKey") = HashKey(drow("name"), drow("password")) Next Return dstKeys End Function Private Shared Function HashKey(ByVal Username As String, ByVal Password As String) As String Return FormsAuthentication.HashPasswordForStoringInConfigFile( Username + Password , "md5") End Function Public Shared Function ValidateKey(ByVal Key As String) As Boolean Dim strMatch As String Dim arrMatches() As DataRow strMatch = "AuthKey='" + Key + "'" arrMatches = AuthKeys.Select(strMatch) If arrMatches.Length = 0 Then Return False Else Return True End If End Function Public Shared Function GetUsername(ByVal Key As String) As String Dim strMatch As String Dim arrMatches() As DataRow strMatch = "AuthKey='" + Key + "'" arrMatches = AuthKeys.Select(strMatch) If arrMatches.Length = 0 Then Return String.Empty Else Return arrMatches(0)("name") End If End Function Public Shared Function Authenticate(ByVal Username As String, ByVal Password As String) As Boolean Dim strMatch As String Dim arrMatches() As DataRow Dim userIdentity As GenericIdentity strMatch = String.Format("name='{0}' AND password='{1}'", Username, Password) arrMatches = AuthKeys.Select(strMatch) If arrMatches.Length = 0 Then Return False End If ' Create the User userIdentity = New GenericIdentity(Username, "cookieless") HttpContext.Current.User = New GenericPrincipal( userIdentity, Nothing) Return True End Function Public Shared Sub RedirectFromLoginPage() Dim context As HttpContext Dim urlReturn As String context = HttpContext.Current urlReturn = context.Request("ReturnUrl") If urlReturn = Nothing Then urlReturn = context.Request.ApplicationPath + "/default.aspx" End If urlReturn = AddAuthKey(urlReturn) context.Response.Redirect(urlReturn) End Sub Public Shared Function AddAuthKey(ByVal Url As String) As String If Url.IndexOf("?") = -1 Then Return String.Format("{0}?authKey={1}", Url, GetAuthKey()) Else Return String.Format("{0}&authKey={1}", Url, GetAuthKey()) End If End Function Public Shared Function GetAuthKey() As String Dim context As HttpContext Dim username As String Dim strMatch As String Dim arrMatches() As DataRow context = HttpContext.Current username = context.User.Identity.Name strMatch = "name='" + username + "'" arrMatches = AuthKeys.Select(strMatch) If arrMatches.Length = 0 Then Return String.Empty End If Return arrMatches(0)("AuthKey") End Function End Class
Next, we need to create the XML file that will contain the usernames and passwords:
Add a new XML file to your project named Users.xml
.
Enter the following two usernames and passwords:
<?xml version="1.0" encoding="utf-8" ?> <users> <user name="Steve" password="secret" /> <user name="Bob" password="secret" /> </users>
Next, you need to create the CookielessLogin.aspx page. This is the page to which a user is redirected when a valid authentication key cannot be retrieved.
Add a new Web Form Page to your project named CookielessLogin.aspx
.
Add two TextBox
controls and a Button
control to the page. Assign the ID txtUsername
to the first text box and the ID txtPassword
to the second text box.
Double-click the Button
control to switch to the Code Editor and enter the following code for the Button1_Click
method:
C#.
private void Button1_Click(object sender, System.EventArgs e) { if (CookielessAuthentication.Authenticate(txtUsername.Text, txtPassword.Text)) CookielessAuthentication.RedirectFromLoginPage(); }
VB.NET.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click If CookielessAuthentication.Authenticate(txtUsername.Text, txtPassword.Text) Then CookielessAuthentication.RedirectFromLoginPage() End If End Sub
Build the project by selecting Build Project Name from the Build menu.
Finally, you need to register the cookieless authentication module in the Web.Config file:
Open the Web.Config file from the Solution Explorer window.
Erase the existing content from the Web.Config file (don’t let this make you nervous) and enter the following code:
<configuration> <system.web> <httpModules> <add name="CookielessAuthenticationModule" type="myApp.CookielessAuthenticationModule,myApp" /> </httpModules> </system.web> </configuration>
After you complete these steps, if you attempt to open any page in the same project in a browser, you’ll be automatically redirected to the CookielessLogin.aspx page. If you enter a username and password combination from the Users.xml file, you’ll be redirected back to the original page with an authentication key appended as a query string.
After you’ve implemented the cookieless authentication module, you must be careful to pass the authentication key in every link. If you don’t pass the authentication key, the user will be redirected back to the CookielessLogin.aspx page. You can automatically append the authentication key to any URL by taking advantage of the AddAuthKey
method of the CookielessAuthentication
class.
You can use the performance logging HttpModule to identify especially slow executing pages in a Web application. The module tracks the average execution time of every page by storing this information the cache. If you want to view the execution times of all the pages in your application, you can open the ShowPerformance.aspx page to read the cached data.
Let’s start by creating the HttpModule itself.
Procedure 18.5. C# Steps
Add a new class to your project named PerformanceModule.cs
.
Enter the following code for the PerformanceModule.cs
class:
using System; using System.Web; using System.Collections; namespace myApp { public class PerformanceModule : IHttpModule { public void Init(HttpApplication application) { // Initialize Page Speeds Hashtable application.Context.Cache[ "PageSpeeds" ] = new Hashtable(); // Wireup EndRequest Event Handler application.EndRequest += new EventHandler( EndRequest ); } public void Dispose(){} void EndRequest(Object sender, EventArgs e) { // Calculate Page Execution Time HttpContext context = ((HttpApplication)sender).Context; TimeSpan timeDiff = DateTime.Now - context.Timestamp; // Retrieve Hashtable of Page Speeds from Cache string cacheKey = context.Request.Path.ToLower(); Hashtable colPageSpeeds = (Hashtable)context.Cache[ "PageSpeeds" ]; PageSpeed objPageSpeed = (PageSpeed)colPageSpeeds[cacheKey]; if (objPageSpeed == null) objPageSpeed = new PageSpeed( cacheKey ); // Update Page Execution Time objPageSpeed.Update( timeDiff.Ticks ); // Save Changes to Cache ((Hashtable)context.Cache[ "PageSpeeds" ])[cacheKey] = objPageSpeed; } } }
Procedure 18.6. VB.NET Steps
Add a new class to your project named PerformanceModule.vb
.
Enter the following code for the PerformanceModule.vb
class:
Public Class PerformanceModule Implements IHttpModule Public Sub Init(ByVal application As HttpApplication) Implements IHttpModule.Init ' Initialize Page Speeds Hashtable application.Context.Cache("PageSpeeds") = New Hashtable() ' Wireup EndRequest Event Handler AddHandler application.EndRequest, AddressOf EndRequest End Sub Public Sub Dispose() Implements IHttpModule.Dispose End Sub Sub EndRequest(ByVal sender As Object, ByVal e As EventArgs) Dim context As HttpContext Dim timeDiff As TimeSpan Dim cacheKey As String Dim colPageSpeeds As Hashtable Dim objPageSpeed As PageSpeed ' Calculate Page Execution Time context = sender.Context timeDiff = DateTime.Now.Subtract(context.Timestamp) ' Retrieve Hashtable of Page Speeds from Cache cacheKey = context.Request.Path.ToLower() colPageSpeeds = context.Cache("PageSpeeds") objPageSpeed = colPageSpeeds(cacheKey) If objPageSpeed Is Nothing Then objPageSpeed = New PageSpeed(cacheKey) End If ' Update Page Execution Time objPageSpeed.Update(timeDiff.Ticks) ' Save Changes to Cache context.Cache("PageSpeeds")(cacheKey) = objPageSpeed End Sub End Class
The performance module tracks the execution speed of every page requested in an application. In the Init
method, a hashtable is created in the cache that contains the performance data.
Additionally, the EndRequest
method is wired to the application EndRequest
event.
The execution speed of the page is calculated within the EndRequest
method. The HttpContext.TimeStamp
property is used to retrieve the time when the page started executing. The DateTime.Now
property is used to retrieve the current time. This information is stored in a class named PageSpeed
.
The DateTime.Now
property is only accurate to 10 milliseconds. This means that the module discussed in this section is only valuable when tracking the performance of relatively slow running pages (pages that require more than 10 milliseconds to execute).
If you need more accurate performance statistics, you should take advantage of performance counters from your code. See the following two knowledge base articles available at the Microsoft.com Web site: Q306978 and Q306979.
Next, we need to create the PageSpeed
class. The PageSpeed
class represents the performance information for an individual page.
Procedure 18.7. C# Steps
Add a new class to your project named PageSpeed.cs
.
Add the following code to the PageSpeed.cs page:
using System; namespace myApp { public class PageSpeed { string _path; int _numberOfRequests = 0; long _executionTimeSum = 0; long _executionTimeLast = 0; public PageSpeed(string path ) { _path = path; } public void Update(long executionTime) { _numberOfRequests ++; _executionTimeSum += executionTime; _executionTimeLast = executionTime; } public string Path { get { return _path; } } public int NumberOfRequests { get { return _numberOfRequests; } } public string ExecutionTimeAverage { get { long lngMilliseconds = (_executionTimeSum/_numberOfRequests)/TimeSpan.TicksPerMillisecond; return String.Format( "{0} milliseconds", lngMilliseconds ); } } public string ExecutionTimeLast { get { long lngMilliseconds = _executionTimeLast/TimeSpan.TicksPerMillisecond; return String.Format( "{0} milliseconds", lngMilliseconds ); } } } }
Procedure 18.8. VB.NET Steps
Add a new class to your project named PageSpeed.vb
.
Add the following code to the PageSpeed.vb page:
Public Class PageSpeed Private _path As String Private _numberOfRequests As Integer = 0 Private _executionTimeSum As Long = 0 Private _executionTimeLast As Long = 0 Public Sub New(ByVal path As String) _path = path End Sub Public Sub Update(ByVal executionTime As Long) _numberOfRequests += 1 _executionTimeSum += executionTime _executionTimeLast = executionTime End Sub Public ReadOnly Property Path() As String Get Return _path End Get End Property Public ReadOnly Property NumberOfRequests() As Integer Get Return _numberOfRequests End Get End Property Public ReadOnly Property ExecutionTimeAverage() As String Get Dim lngMilliseconds As Long lngMilliseconds = (_executionTimeSum / _numberOfRequests) / TimeSpan .TicksPerMillisecond Return String.Format("{0} milliseconds", lngMilliseconds) End Get End Property Public ReadOnly Property ExecutionTimeLast() As String Get Dim lngMilliseconds As Long lngMilliseconds = _executionTimeLast / TimeSpan.TicksPerMillisecond Return String.Format("{0} milliseconds", lngMilliseconds) End Get End Property End Class
The PageSpeed
class has one important method named Update
. When Update
is called, the PageSpeed
class updates the average execution time for the page that the class represents.
Next, we need to create a page that displays the performance data:
Create a new Web Form Page named ShowPerformance.aspx
.
Add a DataGrid to the page.
Double-click the Designer surface to switch to the Code Editor.
Enter the following code for the Page_Load
method:
C#.
private void Page_Load(object sender, System.EventArgs e) { Hashtable colPageSpeeds = (Hashtable)Cache["PageSpeeds"]; DataGrid1.DataSource = colPageSpeeds.Values; DataGrid1.DataBind(); }
VB.NET.
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim colPageSpeeds As Hashtable colPageSpeeds = Cache("PageSpeeds") DataGrid1.DataSource = colPageSpeeds.Values DataGrid1.DataBind() End Sub
Finally, we need to register the performance module in the Web.Config file:
Erase all the current contents of the Web.Config file (don’t let this make you nervous) and enter the following code:
<configuration> <system.web> <httpModules> <add name="PerformanceModule" type="myApp.PerformanceModule,myApp" /> </httpModules> </system.web> </configuration>
After you complete these steps, build and browse the ShowPerformance.aspx page. If you press Refresh a couple times, you should see the ShowPerformance.aspx page with its average execution time displayed in the DataGrid. If you open any other page, the new page should appear with performance statistics in the DataGrid as well (see Figure 18.3).
In this chapter, you’ve learned how to handle application events by taking advantage of both the Global.asax file and custom HttpModules. In the first section, you learned how to handle several application-wide events by adding Application_BeginRequest, Application_Error, Application_ResolveRequestCache
, and Session_Start
methods to the Global.asax file.
Next, we tackled the more advanced topic of custom HttpModules. We created two modules—a cookieless authentication module and a performance logging module.