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 abstract 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 abstract class WebEventProvider : ProviderBase
{
    public abstract void ProcessEvent (WebBaseEvent raisedEvent);
    public abstract void Flush ();
    public abstract void Shutdown ();
}

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 virtuals 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

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

public class TextFileWebEventProvider : WebEventProvider
{
    private string _LogFileName;
    
    public override void Initialize(string name,
      NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

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

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description", "Text file Web event provider");
        }

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

        // Initialize _LogFileName and make sure the path
        // is app-relative
        string path = config["logFileName"];

        if (String.IsNullOrEmpty(path))
            throw new ProviderException
                ("Missing logFileName attribute");

        if (!VirtualPathUtility.IsAppRelative(path))
            throw new ArgumentException
                ("logFileName must be app-relative");

        string fullyQualifiedPath = 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
        FileIOPermission permission =
            new FileIOPermission(FileIOPermissionAccess.Write |
            FileIOPermissionAccess.Append, _LogFileName);
        permission.Demand();

        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        {
            string attr = config.GetKey(0);
            if (!String.IsNullOrEmpty(attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }
    }

    public override void ProcessEvent(WebBaseEvent raisedEvent)
    {
        // Write an entry to the log file
        LogEntry (FormatEntry(raisedEvent));
    }

    public override void Flush() {}
    public override void Shutdown() {}

    // Helper methods
    private string FormatEntry(WebBaseEvent e)
    {
        return String.Format("{0}\t{1}\t{2} (Event Code: {3})",
            e.EventTime, e.GetType ().ToString (), e.Message,
            e.EventCode);
    }

    private void LogEntry(string entry)
    {
        StreamWriter writer = null;

        try
        {
            writer = new StreamWriter(_LogFileName, true);
            writer.WriteLine(entry);
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }
}

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 abstract methods, most notably a default implementation of ProcessEvent that buffers Web events in a WebEventBuffer object if buffering is enabled. It also adds an abstract 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 abstract class BufferedWebEventProvider : WebEventProvider
{
    // Properties
    public bool UseBuffering { get; }
    public string BufferMode { get; }

    // Virtual methods
    public override void Initialize (string name,
        NameValueCollection config);
    public override void ProcessEvent (WebBaseEvent raisedEvent);
    public override void Flush ();

    // Abstract methods
    public abstract void ProcessEventFlush (WebEventBufferFlushInfo
        flushInfo);
}

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 abstract (MustOverride in Visual Basic) 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 abstract 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 virtuals 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

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

public class BufferedTextFileWebEventProvider :
    BufferedWebEventProvider
{
    private string _LogFileName;
    
    public override void Initialize(string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

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

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description",
                "Buffered text file Web event provider");
        }

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

        string path = config["logFileName"];

        if (String.IsNullOrEmpty(path))
            throw new ProviderException
                ("Missing logFileName attribute");

        if (!VirtualPathUtility.IsAppRelative(path))
            throw new ArgumentException
                ("logFileName must be app-relative");

        string fullyQualifiedPath = 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
        FileIOPermission permission =
            new FileIOPermission(FileIOPermissionAccess.Write |
            FileIOPermissionAccess.Append, _LogFileName);
        permission.Demand();

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

        // NOTE: No need to check for unrecognized attributes
        // here because base.Initialize has already done it
    }

    public override void ProcessEvent(WebBaseEvent raisedEvent)
    {
        if (UseBuffering)
        {
            // If buffering is enabled, call the base class's
            // ProcessEvent method to buffer the event
            base.ProcessEvent(raisedEvent);
        }
        else
        {
            // If buffering is not enabled, log the Web event now
            LogEntry(FormatEntry(raisedEvent));
        }
    }

    public override void ProcessEventFlush (WebEventBufferFlushInfo
        flushInfo)
    {
        // Log the events buffered in flushInfo.Events
        foreach (WebBaseEvent raisedEvent in flushInfo.Events)
            LogEntry (FormatEntry(raisedEvent));
    }
    
    public override void Shutdown()
    {
        Flush();
    }

    // Helper methods
    private string FormatEntry(WebBaseEvent e)
    {
        return String.Format("{0}\t{1}\t{2} (Event Code: {3})",
            e.EventTime, e.GetType ().ToString (), e.Message,
            e.EventCode);
    }

    private void LogEntry(string entry)
    {
        StreamWriter writer = null;

        try
        {
            writer = new StreamWriter(_LogFileName, true);
            writer.WriteLine(entry);
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }
}

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: