Web Event Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Web event providers provide the interface between ASP.NET's health monitoring subsystem and data sources that log or further process the events ("Web events") fired by that subsystem. The most common reason for writing a custom Web event provider is to enable administrators to log Web events in media not supported by the built-in Web event providers. ASP.NET 2.0 comes with Web event providers for logging Web events in the Windows event log (EventLogWebEventProvider) and in Microsoft SQL Server databases (SqlWebEventProvider). It also includes Web event providers that respond to Web events by sending e-mail (SimpleMailWebEventProvider and TemplatedMailWebEventProvider) and by forwarding them to the WMI subsystem (WmiWebEventProvider) and to diagnostics trace (TraceWebEventProvider).

Developers writing custom Web event providers generally begin by deriving from System.Web.Management.WebEventProvider, which derives from ProviderBase and adds mustoverride methods and properties defining the basic characteristics of a Web event provider, or from System.Web.Management.BufferedWebEventProvider, which derives from WebEventProvider and adds buffering support. (SqlWebEventProvider, for example, derives from BufferedWebEventProvider so events can be "batched" and committed to the database en masse.) Developers writing Web event providers that send e-mail may also derive from System.Web.Management.MailWebEventProvider, which is the base class for SimpleMailWebEventProvider and TemplatedMailWebEventProvider.

The WebEventProvider Class

System.Web.Management.WebEventProvider is prototyped as follows:

Public MustInherit Class WebEventProvider 
Inherits ProviderBase
  
  Public MustOverride Sub ProcessEvent(ByVal raisedEvent As WebBaseEvent)
  Public MustOverride Sub Flush()
  Public MustOverride Sub Shutdown()
End Class

The following table describes WebEventProvider's members and provides helpful notes regarding their implementation:

MethodDescription
ProcessEventCalled by ASP.NET when a Web event mapped to this provider fires. The raisedEvent parameter encapsulates information about the Web event, including the event type, event code, and a message describing the event.
FlushNotifies providers that buffer Web events to flush their buffers. Called by ASP.NET when System.Web.Management.WebEventManager.Flush is called to flush buffered events.
ShutdownCalled by ASP.NET when the provider is unloaded. Use it to release any unmanaged resources held by the provider or to perform other clean-up operations.

Your job in implementing a custom Web event provider in a derived class is to override and provide implementations of WebEventProvider's abstract methods, and optionally to override key overrides such as Initialize.

TextFileWebEventProvider

Listing 1 contains the source code for a sample Web event provider named TextFileWebEventProvider that logs Web events in a text file. The text file's name is specified using the provider's logFileName attribute, and the text file is automatically created by the provider if it doesn't already exist. A new entry is written to the log each time TextFileWebEventProvider's ProcessEvent method is called notifying the provider that a Web event has been fired.

Listing 1. TextFileWebEventProvider

Imports System
Imports System.Web.Management
Imports System.Configuration.Provider
Imports System.Collections.Specialized
Imports System.Web.Hosting
Imports System.IO
Imports System.Security.Permissions
Imports System.Web

Public Class TextFileWebEventProvider 
Inherits WebEventProvider
    Private _LogFileName As String

    Public Overrides Sub Initialize(ByVal name As String, _
            ByVal config As NameValueCollection)
        ' Verify that config isn't null
        If config Is Nothing Then
            Throw New ArgumentNullException("config")
        End If

        ' Assign the provider a default name if it doesn't have one
        If String.IsNullOrEmpty(name) Then
            name = "TextFileWebEventProvider"
        End If

        ' Add a default "description" attribute to config if the
        ' attribute doesn't exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", "Text file Web event provider")
        End If

        ' Call the base class's Initialize method
        MyBase.Initialize(name, config)

        ' Initialize _LogFileName and make sure the path
        ' is app-relative
        Dim path As String = config("logFileName")

        If String.IsNullOrEmpty(path) Then
            Throw New ProviderException( _
                "Missing logFileName attribute")
        End If

        If (Not VirtualPathUtility.IsAppRelative(path)) Then
            Throw New ArgumentException( _
                "logFileName must be app-relative")
        End If

        Dim fullyQualifiedPath As String = _
            VirtualPathUtility.Combine( _
                VirtualPathUtility.AppendTrailingSlash( _
                HttpRuntime.AppDomainAppVirtualPath), path)

        _LogFileName = HostingEnvironment.MapPath(fullyQualifiedPath)
        config.Remove("logFileName")

        ' Make sure we have permission to write to the log file
        ' throw an exception if we don't
        Dim permission As FileIOPermission = New FileIOPermission( _
            FileIOPermissionAccess.Write Or _
            FileIOPermissionAccess.Append, _LogFileName)
        permission.Demand()

        ' Throw an exception if unrecognized attributes remain
        If config.Count > 0 Then
            Dim attr As String = config.GetKey(0)
            If (Not String.IsNullOrEmpty(attr)) Then
                Throw New ProviderException( _
                "Unrecognized attribute: " & attr)
            End If
        End If
    End Sub

    Public Overrides Sub ProcessEvent( _
        ByVal raisedEvent As WebBaseEvent)
        ' Write an entry to the log file
        LogEntry(FormatEntry(raisedEvent))
    End Sub

    Public Overrides Sub Flush()
    End Sub
    Public Overrides Sub Shutdown()
    End Sub

    ' Helper methods
    Private Function FormatEntry(ByVal e As WebBaseEvent) As String
        Return String.Format("{0}" & Constants.vbTab & _
            "{1}" & Constants.vbTab & "{2} (Event Code: {3})", _
            e.EventTime, e.GetType().ToString(), e.Message, _
            e.EventCode)
    End Function

    Private Sub LogEntry(ByVal entry As String)
        Dim writer As StreamWriter = Nothing

        Try
            writer = New StreamWriter(_LogFileName, True)
            writer.WriteLine(entry)
        Finally
            If Not writer Is Nothing Then
                writer.Close()
            End If
        End Try
    End Sub
End Class

The Web.config file in Listing 2 registers TextFileWebEventProvider as a Web event provider and maps it to Application Lifetime events-one of several predefined Web event types fired by ASP.NET. Application Lifetime events fire at key junctures during an application's lifetime, including when the application starts and stops.

Listing 2. Web.config file mapping Application Lifetime events to TextFileWebEventProvider

<configuration>
  <system.web>
    <healthMonitoring enabled="true">
      <providers>
        <add name="AspNetTextFileWebEventProvider"
          type="TextFileWebEventProvider"
          logFileName="~/App_Data/Contosolog.txt"
        />
      </providers>
      <rules>
        <add name="Contoso Application Lifetime Events"
          eventName="Application Lifetime Events"
          provider="AspNetTextFileWebEventProvider"
          minInterval="00:00:01" minInstances="1"
          maxLimit="Infinite"
        />
      </rules>
    </healthMonitoring>
  </system.web>
</configuration>

Listing 3 shows a log file generated by TextFileWebEventProvider, with tabs transformed into line breaks for formatting purposes. (In an actual log file, each entry comprises a single line.) An administrator using this log file now has a written record of application starts and stops.

Listing 3: Sample log produced by TextFileWebEventProvider

5/12/2005 5:56:05 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is starting. (Event Code: 1001)

5/12/2005 5:56:16 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is shutting down. Reason: Configuration changed. (Event Code: 1002)

5/12/2005 5:56:16 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is shutting down. Reason: Configuration changed. (Event Code: 1002)

5/12/2005 5:56:19 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is starting. (Event Code: 1001)

5/12/2005 5:56:23 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is shutting down. Reason: Configuration changed. (Event Code: 1002)

5/12/2005 5:56:23 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is shutting down. Reason: Configuration changed. (Event Code: 1002)

5/12/2005 5:56:26 PM
System.Web.Management.WebApplicationLifetimeEvent
Application is starting. (Event Code: 1001)

The BufferedWebEventProvider Class

One downside to TextFileWebEventProvider is that it opens, writes to, and closes a text file each time a Web event mapped to it fires. That might not be bad for Application Lifetime events, which fire relatively infrequently, but it could adversely impact the performance of the application as a whole if used to log events that fire more frequently (for example, in every request).

The solution is to do as SqlWebEventProvider does and derive from BufferedWebEventProvider rather than WebEventProvider. BufferedWebEventProvider adds buffering support to WebEventProvider. It provides default implementations of some of WebEventProvider's mustoverride methods, most notably a default implementation of ProcessEvent that buffers Web events in a WebEventBuffer object if buffering is enabled. It also adds an mustoverride method named ProcessEventFlush that's called when buffered Web events need to be unbuffered. And it adds properties named UseBuffering and BufferMode (complete with implementations) that let the provider determine at run-time whether buffering is enabled and, if it is, what the buffering parameters are.

System.Web.Management.BufferedWebEventProvider is prototyped as follows:

Public MustInherit Class BufferedWebEventProvider 
   Inherits WebEventProvider

  ' Properties
  Public Property Readonly UseBuffering As Boolean
  Public Property Readonly BufferMode As String

  ' overrides methods
  Public Overrides Sub Initialize ( _
   name As String, _
   config As NameValueCollection _
  )

  Public Overrides Sub ProcessEvent ( _
   eventRaised As WebBaseEvent _
  )

  public overrides void Flush ()

  ' mustoverride methods
  Public MustOverride Sub ProcessEventFlush ( _
   flushInfo As WebEventBufferFlushInfo _
  )

End Class

The following table describes BufferedWebEventProvider's members and provides helpful notes regarding their implementation:

Method or PropertyDescription
UseBufferingBoolean property that specifies whether buffering is enabled. BufferedWebEventProvider.Initialize initializes this property from the buffer attribute of the <add> element that registers the provider. UseBuffering defaults to true.
BufferModeString property that specifies the buffer mode. BufferedWebEventProvider.Initialize initializes this property from the bufferMode attribute of the <add> element that registers the provider. bufferMode values are defined in the <bufferModes> section of the <healthMonitoring> configuration section. This property has no default value. BufferedWebEventProvider.Initialize throws an exception if UseBuffering is true but the bufferMode attribute is missing.
InitializeOverridden by BufferedWebEventProvider. The default implementation initializes the provider's UseBuffering and BufferMode properties, calls base.Initialize, and then throws an exception if unprocessed configuration attributes remain in the config parameter's NameValueCollection.
ProcessEventOverridden by BufferedWebEventProvider. The default implementation calls ProcessEventFlush if buffering is disabled (that is, if UseBuffering is false) or adds the event to an internal WebEventBuffer if buffering is enabled.
FlushOverridden by BufferedWebEventProvider. The default implementation calls Flush on the WebEventBuffer holding buffered Web events. WebEventBuffer.Flush, in turn, conditionally calls ProcessEventFlush using internal logic that takes into account, among other things, the current buffer mode and elapsed time.
ProcessEventFlushCalled by ASP.NET to flush buffered Web events. The WebEventBufferFlushInfo parameter passed to this method includes, among other things, an Event property containing a collection of buffered Web events.

This method is MustOverride and must be overridden in a derived class.

Your job in implementing a custom buffered Web event provider in a derived class is to override and provide implementations of BufferedWebEventProvider's mustoverride methods, including the Shutdown method, which is inherited from WebEventProvider but not overridden by BufferedWebEventProvider, and ProcessEventFlush, which exposes a collection of buffered Web events that you can iterate over. Of course, you can also override key overridables such as Initialize.

BufferedTextFileWebEventProvider

Listing 4 contains the source code for a sample buffered Web event provider named BufferedTextFileWebEventProvider, which logs Web events in a text file just like TextFileWebEventProvider. However, unlike TextFileWebEventProvider, BufferedTextFileWebEventProvider doesn't write to the log file every time it receives a Web event. Instead, it uses the buffering support built into BufferedWebEventProvider to cache Web events. If UseBuffering is true, BufferedTextFileWebEventProvider commits Web events to the log file only when its ProcessEventFlush method is called.

Listing 4. BufferedTextFileWebEventProvider

Imports System
Imports System.Web.Management
Imports System.Configuration.Provider
Imports System.Collections.Specialized
Imports System.Web.Hosting
Imports System.IO
Imports System.Security.Permissions
Imports System.Web

Public Class BufferedTextFileWebEventProvider
    Inherits BufferedWebEventProvider

    Private _LogFileName As String

    Public Overrides Sub Initialize(ByVal name As String, _
        ByVal config As NameValueCollection)
        ' Verify that config isn't null
        If config Is Nothing Then
            Throw New ArgumentNullException("config")
        End If

        ' Assign the provider a default name if it doesn't have one
        If String.IsNullOrEmpty(name) Then
            name = "BufferedTextFileWebEventProvider"
        End If

        ' Add a default "description" attribute to config if the
        ' attribute doesn't exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", _
            "Buffered text file Web event provider")
        End If

        ' Initialize _LogFileName. NOTE: Do this BEFORE calling the
        ' base class's Initialize method. BufferedWebEventProvider's
        ' Initialize method checks for unrecognized attributes and
        ' throws an exception if it finds any. If we don't process
        ' logFileName and remove it from config, base.Initialize will
        ' throw an exception.

        Dim path As String = config("logFileName")

        If String.IsNullOrEmpty(path) Then
            Throw New ProviderException( _
            "Missing logFileName attribute")
        End If

        If (Not VirtualPathUtility.IsAppRelative(path)) Then
            Throw New ArgumentException( _
            "logFileName must be app-relative")
        End If

        Dim fullyQualifiedPath As String = _
            VirtualPathUtility.Combine( _
            VirtualPathUtility.AppendTrailingSlash( _
            HttpRuntime.AppDomainAppVirtualPath), path)

        _LogFileName = HostingEnvironment.MapPath(fullyQualifiedPath)
        config.Remove("logFileName")

        ' Make sure we have permission to write to the log file
        ' throw an exception if we don't
        Dim permission As FileIOPermission = New FileIOPermission( _
            FileIOPermissionAccess.Write Or _
            FileIOPermissionAccess.Append, _LogFileName)
        permission.Demand()

        ' Call the base class's Initialize method
        MyBase.Initialize(name, config)

        ' NOTE: No need to check for unrecognized attributes
        ' here because base.Initialize has already done it
    End Sub

    Public Overrides Sub ProcessEvent( _
        ByVal raisedEvent As WebBaseEvent)
        If UseBuffering Then
            ' If buffering is enabled, call the base class's
            ' ProcessEvent method to buffer the event
            MyBase.ProcessEvent(raisedEvent)
        Else
            ' If buffering is not enabled, log the Web event now
            LogEntry(FormatEntry(raisedEvent))
        End If
    End Sub

    Public Overrides Sub ProcessEventFlush( _
        ByVal flushInfo As WebEventBufferFlushInfo)
        ' Log the events buffered in flushInfo.Events
        For Each raisedEvent As WebBaseEvent In flushInfo.Events
            LogEntry(FormatEntry(raisedEvent))
        Next raisedEvent
    End Sub

    Public Overrides Sub Shutdown()
        Flush()
    End Sub

    ' Helper methods
    Private Function FormatEntry(ByVal e As WebBaseEvent) As String
        Return String.Format("{0}" & Constants.vbTab & _
        "{1}" & Constants.vbTab & "{2} (Event Code: {3})", _
        e.EventTime, e.GetType().ToString(), e.Message, e.EventCode)
    End Function

    Private Sub LogEntry(ByVal entry As String)
        Dim writer As StreamWriter = Nothing

        Try
            writer = New StreamWriter(_LogFileName, True)
            writer.WriteLine(entry)
        Finally
            If Not writer Is Nothing Then
                writer.Close()
            End If
        End Try
    End Sub
End Class

One notable aspect of BufferedTextFileWebEventProvider's implementation is that its Initialize method processes the logFileName configuration attribute before calling base.Initialize, not after. The reason why is important. BufferedWebEventProvider's Initialize method throws an exception if it doesn't recognize one or more of the configuration attributes in the config parameter. Therefore, custom attributes such as logFileName must be processed and removed from config before the base class's Initialize method is called. In addition, there's no need for BufferedTextFileWebEventProvider's own Initialize method to check for unrecognized configuration attributes since that check is performed by the base class.

Inside the ASP.NET Team
BufferedWebEventProvider's Initialize method is inconsistent with other providers' Initialize implementations in its handling of configuration attributes. The difference isn't critical, but it is something that provider developers should be aware of. The reason for the inconsistency is simple and was summed up this way by an ASP.NET dev lead:

"Different devs wrote different providers and we didn't always manage to herd the cats."

That's something any dev who has worked as part of a large team can appreciate.

The Web.config file in Listing 5 registers BufferedTextFileWebEventProvider as a Web event provider and maps it to Application Lifetime events. Note the bufferMode attribute setting the buffer mode to "Logging." "Logging" is one of a handful of predefined buffer modes; you can examine them all in ASP.NET's default configuration files. If desired, additional buffer modes may be defined in the <bufferModes> section of the <healthMonitoring> configuration section. If no buffer mode is specified, the provider throws an exception. That behavior isn't coded into BufferedTextFileWebEventProvider, but instead is inherited from BufferedWebEventProvider.

Listing 5. Web.config file mapping Application Lifetime events to BufferedTextFileWebEventProvider

<configuration>
  <system.web>
    <healthMonitoring enabled="true">
      <providers>
        <add name="AspNetBufferedTextFileWebEventProvider"
          type="BufferedTextFileWebEventProvider"
          logFileName="~/App_Data/Contosolog.txt"
          bufferMode="Logging"
        />
      </providers>
      <rules>
        <add name="Contoso Application Lifetime Events"
          eventName="Application Lifetime Events"
          provider="AspNetBufferedTextFileWebEventProvider"
          minInterval="00:00:01" minInstances="1"
          maxLimit="Infinite"
        />
      </rules>
    </healthMonitoring>
  </system.web>
</configuration>

Click here to continue on to part 7, Web Parts Personalization Providers.

Show: