Chapter 18. Handling Application-Wide Events

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

Using the Global.asax File

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.

Global.asax file in Design view.

Figure 18.1. Global.asax file in Design view.

By default, the Global.asax file contains the following event handlers:

  • Application_StartRaised when an application starts (for example, after rebooting the Web server or modifying the Global.asax file).

  • Session_StartRaised when a new user requests the first page.

  • Application_BeginRequestRaised whenever any page is requested.

  • Application_AuthenticateRequestRaised after a user has been identified. Use this method to implement custom roles with Forms authentication (see Chapter 16, “Securing Your Application”).

  • Application_ErrorRaised when any unhandled exception occurs in the application.

  • Session_EndRaised when a user session ends (by default, 20 minutes after a user has made the last page request).

  • Application_EndRaised when an application is stopped.

Note

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.

Warning

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.

Handling Application-Wide Errors

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
C#, 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
VB.NET, 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:

  1. Add a new Web Form Page to your project named CauseError.aspx.

  2. Switch to the Code Editor by double-clicking the Designer surface.

  3. 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
    VB.NET MyBase.Load
      Dim Ka
    
      Ka.Boom()
    End Sub
    
  4. 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.

Rewriting Page Requests

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.

  1. Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.

  2. 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
    
  3. 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.

Tip

When you use Context.RewritePath to rewrite the page path, you can use Request.RawUrl to get the original page path.

Detecting the Start and End of a User Session

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.

  1. Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.

  2. 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

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
      Application.Lock()
      Application("numUsers") += 1
      Application.UnLock()
    End Sub
    
  3. 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
    
  4. 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:

  1. Add a new Web Form Page to your project named TrackSessions.aspx.

  2. Add a Web Form Label to the page.

  3. Switch to the Code Editor by double-clicking the Designer surface.

  4. 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
    VB.NET MyBase.Load
      Label1.Text = "There are "
      Label1.Text &= Application("numUsers")
      Label1.Text &= " active sessions"
    End Sub
    
  5. 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.

The TrackSessions.aspx page.

Figure 18.2. The TrackSessions.aspx page.

Note

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.

Implementing Custom Caching

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.

  1. Open the Global.asax file from the Solution Explorer window and switch to the Code Editor.

  2. 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
    
  3. 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
    
  4. 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.

Warning

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:

  1. Right-click the folder containing the pages that you want to cache and select Properties.

  2. Select the Security Tab.

  3. 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.

Using Custom HttpModules

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:

  • InitUsed to initialize any resources and to wire-up any event handlers needed by the HttpModule

  • DisposeUsed to release any resources used the HttpModule

In the following sections, we’ll create two modules—an authentication module and a performance logging module.

Creating the Cookieless Authentication Modules

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

  1. Add a new class to your project named CookielessAuthenticationModule.cs.

  2. 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

  1. Add a new class to your project named CookielessAuthenticationModule.vb.

  2. 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

  1. Add a new class to your project named CookielessAuthentication.cs.

  2. 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

  1. Add a new class to your project named CookielessAuthentication.vb.

  2. 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
    VB.NET Steps String
            Return FormsAuthentication.HashPasswordForStoringInConfigFile( Username + Password
    VB.NET Steps, "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
    VB.NET Steps 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:

  1. Add a new XML file to your project named Users.xml.

  2. 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.

  1. Add a new Web Form Page to your project named CookielessLogin.aspx.

  2. 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.

  3. 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)
    VB.NET Handles Button1.Click
      If CookielessAuthentication.Authenticate(txtUsername.Text, txtPassword.Text) Then
         CookielessAuthentication.RedirectFromLoginPage()
      End If
    End Sub
    
  4. 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:

  1. Open the Web.Config file from the Solution Explorer window.

  2. 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.

Creating the Performance Logging Module

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

  1. Add a new class to your project named PerformanceModule.cs.

  2. 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

  1. Add a new class to your project named PerformanceModule.vb.

  2. 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.

Note

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

  1. Add a new class to your project named PageSpeed.cs.

  2. 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

  1. Add a new class to your project named PageSpeed.vb.

  2. 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
    VB.NET Steps.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:

  1. Create a new Web Form Page named ShowPerformance.aspx.

  2. Add a DataGrid to the page.

  3. Double-click the Designer surface to switch to the Code Editor.

  4. 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
    VB.NET 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:

  1. Open the Web.Config file from the Solution Explorer window.

  2. 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).

The ShowPerformance.aspx page.

Figure 18.3. The ShowPerformance.aspx page.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset