Magazine > Issues > 2003 > May >  Build a Logging and Event Viewing Library to De...
Debugging Tool
Build a Logging and Event Viewing Library to Help Debug Your .NET Framework-based App
Daryn Kiely
Code download available at: LoggingTool.exe (131 KB)
Browse the Code Online

This article assumes you're familiar with C# and Win32
Level of Difficulty 1 2 3
SUMMARY
Building a basic, reusable application framework can make development quicker and easier. This allows you to focus more on the problems at hand and less on the repetitive tasks involved in building any application. In this article, the author presents a framework that provides facilities to access the registry and an extensible framework for logging messages to a console window or the Event Viewer. This reusable framework can be included as a library in your projects, allowing you to display an enhanced, color-coded message log and dynamically change logging levels.
A reusable application framework can take care of the mundane tasks associated with every project, regardless of the programming language you have chosen. This framework usually takes shape early on as a way to store and retrieve application configuration data and as a logging mechanism. How you can build such a framework will be the focus of this article.
In the first section I'll cover storing and retrieving configuration data. I don't know about you, but I've gotten used to storing configuration data in the system registry. The Microsoft® .NET Framework makes it easier by providing the RegistryKey class, which I will expand to provide more functionality and default behavior that is not included as part of the .NET Framework registry classes.
Following the discussion of the registry classes, I'll cover a logging mechanism. Yes, you simply use the existing Event Viewer logging mechanism provided by the .NET Framework, but this can be cumbersome if you are writing and debugging an application. I prefer to see my messages as they are output, without having to scroll through the event viewer. Trace listeners are another option but they don't provide some of the functionality my examples afford you, such as the ability to dynamically change logging levels and logging messages as different levels.

Framework Exception
When defining my application framework, one of the first things I did was define an exception class to deal with exceptions that any internal classes will throw when an error condition occurs. I could have simply used one of the existing exception classes, but by defining my own, you'll get more granular control over which exceptions you want to handle.
Defining a custom exception class is actually very simple. All you have to do is derive from ApplicationException and provide the appropriate constructors. You can add more information to the exception and do your own processing, but for my purposes this would have been overkill.

The Registry Class
The registry classes supplied by the .NET Framework generally do just about everything a developer needs—they provide facilities to read and write to the registry. One thing I wanted to do was wrap the classes to hide some of the implementation details that deal with the commonly accepted rules of registry access. In addition to providing a wrapper for the existing .NET Framework registry classes, I wanted to add some functionality such as monitoring a registry key for changes (discussed later in this article).
There are two recommended places for an application to store registry information: HKEY_LOCAL_MACHINE\Software and HKEY_CURRENT_USER\Software. For client applications, the registry settings should be put under HKEY_CURRENT_USER, and systems applications (such as services) should put their entries under HKEY_LOCAL_MACHINE. If a nontrusted application attempts to modify the registry under HKEY_LOCAL_MACHINE, it will fail because of the built-in security in the .NET Framework and Windows NT®.
Your keys should be put under a manufacturer-specific key in the Software subkey of the registry location you chose to use. The constructor for the registry class will determine these values and open the proper key. It does this through the System.Windows.Forms.Application class and its static members, CommonAppDataRegistry and UserAppDataRegistry (see Figure 1). These static members get the CompanyName, ProductName, and ProductVersion from the assembly information that is stored as part of the application in order to create the base key.
public enum BASE_KEY 
{
    SYSTEM,
    USER
};
public DRegistry()
{
    GetBaseSubKey(BASE_KEY.SYSTEM);
}
public DRegistry(BASE_KEY bkey)
{
    GetBaseSubKey(bkey);
}
public DRegistry(BASE_KEY bkey, bool RawAccess)
{
    switch (bkey) 
    {
        case BASE_KEY.SYSTEM :
            m_CurrentKey = Registry.LocalMachine;
            break;
        case BASE_KEY.USER :
            m_CurrentKey = Registry.CurrentUser;
            break;
    }
    if (!RawAccess) 
    {
        GetBaseSubKey(bkey);
    }
    else 
    {
        m_BaseKey = m_CurrentKey;
    }
}
private void GetBaseSubKey(BASE_KEY bkey) 
{
    switch (bkey) 
    {
        case BASE_KEY.SYSTEM :
            m_CurrentKey = Application.CommonAppDataRegistry;
            break;
        case BASE_KEY.USER :
            m_CurrentKey = Application.UserAppDataRegistry;
            break;
    }
    m_BaseKey = m_CurrentKey;
    if (null == m_CurrentKey) 
    {
        throw (new FrameworkException("DRegistry: Failed getting base 
               subkey"));
    }
}
You will also notice in Figure 1 that I have provided three constructors. The default constructor assumes that all of the data will be placed under the HKEY_LOCAL_MACHINE\Software key with the CompanyName\ProductName\Version subkeys. The second constructor allows the user to choose between HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER. This selection is performed by using an enum that is defined within the registry class. The third constructor extends the functionality of the second constructor to allow the user to specify whether raw registry access is required. If the user does not want to be bound to the defined subkey, they can specify true for the second argument of this constructor. That will open the base key rather than the subkey.
The GetValue and SetValue methods offer pretty much the same functionality as their RegistryKey counterparts, but with a couple of exceptions. I wanted to provide default values for a key if it didn't exist in the registry. The GetValue methods of the registry classes do provide a default value implementation, but unfortunately these never get written back to the registry. I designed my GetValue methods to write the key if there is no current value, or if the value is of the wrong type. The other main difference is that the RegistryKey classes provide one implementation that either takes an object or returns an object. Since I wanted to have more granular control in the GetValue methods, I provided two implementations: one to be used for strings and one to be used for Int32 values. To keep the implementation consistent, I did the same thing with the SetValue methods.
The registry class needs to traverse the registry in order to be useful. When the registry object is created, the key that gets opened is stored in the m_BaseKey member. All navigation through the registry will begin at this key. By calling the GotoKey method the user can easily get a specific subkey, which will be created if it doesn't exist. The GotoKey method is nothing more than a wrapper around the CreateSubKey method of the RegistryKey class.
Finally, I wanted to add the ability to monitor a key and its subkeys. In C++ it was just a matter of making a call to a Win32® API function and supplying an event. Unfortunately this same functionality never appeared as a part of the registry classes in the Microsoft .NET Framework. However, the .NET Framework provides a means to use an existing set of APIs or COM components through Interop. Using the DllImport attribute, it is easy to call Win32 API functions.
There are a couple of catches when working with Interop in this way. The first is that the types that are passed to many of the Win32 calls don't match the native common language runtime (CLR) types. The only thing you can do in this case is to find the type that most closely matches the type that the API call is expecting and pass that as an argument. If a more complex type such as a structure is expected, you can create a structure and use the StructLayout attribute to force it to have the same byte layout as it would in C++.
The next catch is when there are constant values defined in the header files. For the most part, this is more of an inconvenience than anything else. All you need to do is create a const with the same value as the define. The downside to doing this is that if the define ever changes in the external DLL, your code will break.
By using the DLLImport attribute, I exposed the Win32 API function for monitoring registry keys (see Figure 2). In order to make this work, I needed to change the HANDLEs and BOOLs to IntPtrs and ints, respectively.
[DllImport("advapi32.dll")]
static extern Int32 RegNotifyChangeKeyValue(
    IntPtr hKey,          // handle to key to watch
    int bWatchSubtree,    // subkey notification option
    Int32 dwNotifyFilter, // changes to be reported
    IntPtr hEvent,        // handle to event to be signaled
    int fAsynchronous     // asynchronous reporting option
    );

public bool MonitorKey(WaitHandle WaitEv)
{
    const int NotifyFilter = 0xF; // Notify on any change
    IntPtr hkey = GetKeyHandle();
    if (0 == 
      RegNotifyChangeKeyValue(hkey,1,NotifyFilter,WaitEv.Handle,1)) 
    {
        return true;
    }
    return false;
}

private IntPtr GetKeyHandle()
{
    FieldInfo fi;
    IntPtr hkey;

    Type regKeyType = typeof(RegistryKey);
    fi = regKeyType.GetField("hkey", 
      BindingFlags.Instance|BindingFlags.NonPublic);
    hkey = (IntPtr) fi.GetValue(m_CurrentKey);
    string s = hkey.ToString();
    Int32 i32 = hkey.ToInt32();
    return (IntPtr) hkey;
}
The next issue is how to actually get handles for the event and registry key. As it turns out, getting the event handle is trivial; there is a property on a WaitHandle that will return the operating system kernel handle. Getting the registry key handle, however, was not so easy. In this case I needed to resort to reflection. Using ILDASM, I found that the handle was actually stored as a private member called hkey (see Figure 3). From there I could use Reflection APIs to query its value, as shown in Figure 2. The main drawback is that I am accessing a private member of a class, which breaks the rules of object-oriented programming. In this case, there was no alternative.
Figure 3 Registry Key Handle in ILDASM 

The Logging Class Hierarchy
In designing the logging classes, I wanted to create a framework that other developers could extend. This would allow someone to add a specific logging mechanism, such as sending output to a printer. In order to accomplish this, I would have to create a base class for the logging mechanisms (LogBase) in addition to the logging class (Logger). I would also have to define a class for each of the loggers I wanted to implement, derived from the base class. The inheritance layout is shown in Figure 4. As a foundation, I chose to implement console logging and Event Viewer logging.
Figure 4 My Class Structure 
The framework solution is broken into four distinct projects: the logging base class, the Event Viewer logger, the console logger, and the main logger manager. I broke the project into logical areas of functionality to make it easier to manage and implement.

LogBase and LogMessage
LogBase is a key component that will not only have to implement the details of a thread, but also construct messages and pass them to the specific logging mechanisms. Each of the logging types must derive from LogBase and will have to provide functionality that is unique to the specific logging mechanism in question.
It may seem like an interface would have been the way to go instead of a base class. I didn't go down this path, though, because an interface is by definition a pure abstract base class and I wanted to add some more functionality to it. I wanted each logging mechanism to run as its own thread. This will enable me to run each of the logging threads at a lower priority and lessen the impact of slow logging mechanisms on the application. This also means I will have to implement a buffering mechanism in order to make this effective.
The buffering mechanism is also implemented as part of LogBase. It is nothing more than a Queue, which is defined in the C# collection classes. Everything that is put into this Queue will need to contain two parts, a severity code (which I will explain later) and the message to log. This requires me to create a new class to be called LogMessage, which is nothing more than an object with the information required to log the message and get and set methods for this information (see Figure 5).
public class LogMessage {
    private string m_Message;
    public string Message {
        get {
            return m_Message;
        }
        set {
            m_Message = value;
        }
    }
    private SEV m_Severity;
    public SEV Severity {
        get {
            return m_Severity;
        } 
        set {
            m_Severity = value;
        }
    }
    public LogMessage() {
        m_Severity = SEV.FATAL;
        m_Message = "";
    }
}
The buffering mechanism will also require a way to signal the thread to protect the Queue from concurrent access. In Windows®-based programming with Visual C++®, this is typically done with one of the standard synchronization objects, such as events. In C# the concept is no different, and there are several synchronization classes offered as part of the System.Threading namespace.
Critical sections can be implemented through the Monitor class. The Monitor class is nice because it can synchronize access to a specific memory location. The monitor manages the details of what you are trying to access and when to allow it—all you have to do is pass it an object. If another thread already has access to the object, the first thread will block until it is released with an Exit call. You must ensure that you call Monitor.Exit for each call to Monitor.Enter in order to avoid deadlock.
In addition to a Monitor, I will also use three ManualResetEvents to signal to the thread that there is something to do. The first ManualResetEvent is used to tell the thread that a message is currently on the queue (m_QueueDataEvent). When a thread puts a message on the queue, it sets the event. When the queue length reaches 0, this event is reset. The second ManualResetEvent is used to tell the thread to stop (m_StopEvent), and the final ManualResetEvent is passed to the DRegistry.Monitor method to signal when a change to the registry has occurred.
The Monitor class will control access to both the message queue and the event that is used to signal the thread that there is something to read. By ensuring that all access to the queue and signaling event is encased within the Monitor class's enter and exit methods, I will ensure that all queue operations are protected from concurrent access. If the access is allowed too long on the object, performance will degrade. To avoid this, I do as little work as possible within the Enter/Exit block.
As mentioned earlier, each log will run in its own thread. The entry point for the logging thread is the Run method. This thread spends most of its life blocked, waiting for a stop event, a data ready event, or a registry change event to occur. If the stop event occurs, the logging thread will terminate. If the registry change event occurs, the settings will be refreshed from the registry. If the queue data event occurs, the logger will process a single message from the queue and go back to its wait. If there are multiple messages in the queue, the wait will still return that there are messages to be logged. By cycling around, it gives an opportunity for other threads to add messages to the queue instead of having to wait for all messages to be drained.
Of course, there is a huge flaw with only providing buffered output. If a program crashes while there are some messages still in the buffer, you may never know the real cause of the crash. For this reason, I am providing the facility to force messages of particular severities to be flushed. I'll provide a function that allows the WriteLog method to query the logging facility to see if the message should be flushed. If it should, the thread will directly call the OutputString method, bypassing the queue. There is one subtle flaw with this approach in that flushed messages may precede buffered messages that were sent earlier. For this reason, I must break my earlier rule about spending as little time as possible in the Enter/Exit block. When a message is to be flushed, I must write all the messages that are currently in the queue, followed by the flush message.
Because both the main thread and the logging thread can access the queue, there's a potential race condition. This condition occurs when the thread doing the logging flushes the queue after the logging thread receives a signal that there is a message waiting, but before it reads the queue. If I do not check for this in the logging thread, it is possible that it will attempt to read a message from the queue after it has already been emptied. An easy solution to this problem is to check the queue length before the read, but after the Monitor has been acquired.
When a message is being logged, it should be formatted by the ConstructMessage method of the LogBase class. The formatting precedes the message with the severity of the message and the date and time. The severity string is one of five hardcoded values that are part of the SEV enum that I have defined as part of the logging base classes. I have found that this is more than I ever need. The date and time is taken from the DateTime class, which just happens to have a static property (Now) that returns a DateTime object set to reflect the current date and time on the computer.
Now that the logging features themselves are complete, I need to add a facility to get and store the logging levels. The first thing I want is a facility for a new logger to provide default settings for each of the logging levels. There are a couple ways to accomplish this, such as providing a virtual member function in LogBase or specifying an attribute that can be specified as part of a logging mechanism to provide default settings. Although both methods are equally effective, using attributes makes more sense. An attribute allows you to specify the characteristics of an object. The default logging levels are just that, characteristics of the logging mechanism. Using attributes also makes the job of the person writing the logging mechanism easier.
To create an attribute, the first thing you must do is write a class that derives from the Attribute base class. Typically, an attribute uses the naming convention XxxAttribute. However, when using the attribute, you do not need to specify the Attribute suffix; you can call the attribute either Xxx or XxxAttribute. The only other thing you need to know about attributes has to do with parameters and how they work.
Attributes can have two types of parameters, positional and named. (The DLL name in the DllImport attribute is an example of a positional parameter.) The positional parameters are defined when the attribute class provides a constructor that takes parameters. Positional parameters must be specified first in the list of parameters for the attribute and must be completely satisfied for at least one publicly accessible constructor.
Named parameters are more useful for the logging class attributes. Named parameters do not need to be specified at all, so they are perfect for specifying optional parameters. If they are specified, they can appear in any order in the parameter list. To specify a named parameter, all you need to do is provide a publicly accessible property.
Properties are a wonderful way to make classes more user-friendly. In C++, I would have provided get and set methods for a private member variable, but with C# I can make the members properties and simplify their usage. By providing get and set accessors, the member variables are exposed as properties.
The advantage of using properties is that code can be added to the get and set accessors to do things like range checking and manipulation. This allows the consumer of the class to have a simplified view of the members, while allowing the class implementer to perform special operations. Additionally, the class implementer can control the accessibility of the properties by providing only a get or set accessor.
It is important to know that the constructor for an attribute is called before the named parameters are set, which allows named parameters to be given default values in the constructor. The attribute class will specify OFF as a default value for all the logging levels. By using the named parameters, you can simply override the logging levels that make sense for your usage. The complete DefaultSeveritiesAttribute class is shown in Figure 6.
[AttributeUsage(AttributeTargets.Class)]
public sealed class DefaultSeveritiesAttribute : Attribute
{
    public DefaultSeveritiesAttribute()
    {
        fatal = "OFF";
        fail = "OFF";
        debug = "OFF";
        info = "OFF";
        init = "OFF";
    }
    private bool IsValid(string str)
    {
        switch (str) 
        {
            case "ON" :
            case "OFF" :
            case "FLUSH" :
                return true;
            default :
                return false;
        }
    }
    private string fatal;
    public string FATAL 
    {
        get 
        {
            return fatal;
        }
        set 
        {
            if (IsValid(value)) 
            {
                fatal = value;
            }
        }
    }
    private string fail;
    public string FAIL 
    {
        get 
        {
            return fail;
        }
        set 
        {
            if (IsValid(value)) 
            {
                fail = value;
            }
        }
    }
    private string init;
    public string INIT 
    {
        get 
        {
            return init;
        }
        set 
        {
            if (IsValid(value)) 
            {
                init = value;
            }
        }
    }
    private string info;
    public string INFO 
    {
        get 
        {
            return info;
        }
        set 
        {
            if (IsValid(value)) 
            {
                info = value;
            }
        }
    }
    private string debug;
    public string DEBUG 
    {
        get 
        {
            return debug;
        }
        set 
        {
            if (IsValid(value)) 
            {
                debug = value;
            }
        }
    }
}
Once the default logging levels are set up through the use of the attribute, I'll need a method that can get and make use of the attributes. This will be done in the GetDefaultLevel method, as shown in Figure 7. The first thing you will notice in this method is a call to GetCustomAttribute, which will either return an instance of the attribute class or return null if the class does not have the attribute specified. If the attribute has not been specified, I set the default logging level to OFF, otherwise I simply get the level from the properties in the attribute class.
protected string GetDefaultLevel(SEV severity) 
{
    DefaultSeveritiesAttribute attr = (DefaultSeveritiesAttribute)
    Attribute.GetCustomAttribute(this.GetType(),
        typeof(DefaultSeveritiesAttribute));
    if (null == attr) 
    {
        return "OFF";
    }
    switch (severity) 
    {
        case SEV.DEBUG : 
            return attr.DEBUG;
        case SEV.FAIL :
            return attr.FAIL;
        case SEV.FATAL :
            return attr.FATAL;
        case SEV.INFO :
            return attr.INFO;
        case SEV.INIT :
            return attr.INIT;
    }
    return "OFF";
}
The rest of the logging level setup is very simple. Persistence is achieved either by retrieving from or saving the levels to the registry using the registry class described at the beginning of this article. At run time, the levels are stored in a static member variable that is called m_levels.
In order to allow you to set the program name, I made it a property of both the Logger and LogBase classes. When it is set, the ProgramName property will update the program name within the logger by calling the UpdateProgramName virtual method. In the case of Event Viewer, it will specify an Event Viewer source and also specify the console title in the event of the console log.
The LogBase also provides a virtual method that is called whenever the logging level is changed. Whenever the registry entries that specify the logging levels changes, the LogLevelChanged method is called. This allows the logger to do special processing when the levels are changing. An example of this processing is shown in the console log.
Finally, the LogBase must provide a virtual function that will be called whenever the logging mechanism is being shut down. This virtual function will provide the means for each logger to perform any required cleanup of the resources it is using. I will use this virtual function later to free the console and close the event viewer.

EventViewerLog
The Event Viewer log was pretty trivial to implement. Everything logged as part of the Event Viewer log is put in the application log. Using the program name property allowed me to specify the source of the log shown in Figure 8.
Figure 8 A Log 
EventLog is one of the nice built-in features within the .NET Framework. The EventViewerLog class only had to use the EventViewer class, which is found in the System.Diagnostics namespace.
Your application must be registered with the event log service as a valid source of events before you can use the event logging classes. This is accomplished through the static method call, CreateEventSource. Once your application is registered, you can set the Source property to specify the text that will appear in the source column of the event viewer.
When the event log is created and the source is set, writing an entry is simply a matter of calling the WriteEntry method. There are several overloaded WriteEntry methods, allowing you to specify which of the EventViewer entries you want to fill in. I chose to fill in the type and the string data. Notice I had to map my event severities to three values in the event viewer (Error, Warning, and Information). These are the only values that Event Viewer supports that make sense to the framework I am creating.
In the EventViewerLog I have added exception handling. All of the Event viewer methods can potentially throw Win32 exceptions and ignoring them could cause your application to crash. The exceptions I handled are specifically related to the Win32 error returns that can be expected from the Event Viewer classes.
The final thing to notice in the definition of the Event Viewer class is how I used the DefaultSeveritiesAttribute (see the link at the top of this article for the full source code). Instead of taking the defaults provided by the system, I change them to more reasonable values for the event viewer by specifying this attribute and overriding the FAIL and FATAL members.

ConsoleLog
Why would I want to define a console log mechanism when System.Console pretty much provides the same functionality? I like logging messages of differing severities in different colors, and consoles are not typically included in a Windows-based program. The desired output from the console log is shown in Figure 9.
Figure 9 Output 
I looked at two possibilities for providing console logging: opening a console window or opening a Windows Forms window. Although it seems like it would be fairly trivial to open a Windows Forms window, it requires overhead that is quite complex. I would have to use a modeless window, provide a message pump, and manage a list of messages that are in the display. Why would I want to recreate all this functionality when a console window already provides what I need?
The first problem I experienced when writing the console log was the lack of console support in the C# libraries. I am used to being able to call AllocConsole, FreeConsole, and all the rest of the related console APIs from within my program. Although I could have used InteropServices as I did in the registry class, I chose to use managed C++ instead. This eliminated having to redefine the incompatible types or recreate defines. After all, one of the benefits of the .NET Framework is the interoperability among languages. This interoperability allows me to create a C++ class (ConsoleLog), derived from a C# class (LogBase), that I can use from another C# class (Logger).
Writing a managed C++ class is not very different from writing a standard C++ class. The first thing to familiarize yourself with is the __gc keyword, which tells the compiler that this is a garbage-collected managed class. The benefit to the programmer is that the .NET Framework will manage the lifetime of that class.
C++ provides the keyword #using, which tells the location of the files that contain the metadata classes that you want to import into your program. To properly use #using, you must define where to find the object that is referenced through the #using directive. This is done through the development environment on the project properties page. Selecting General | Resolve #using References in the C/C++ menu item allows you to set a search path for objects that are required through #using.
The first problem I had in writing the console log class was due to some issues with interlanguage interoperability. The base logging class defines the program name as a String type which would be fine, except that to invoke SetConsoleTitle I need a wchar_t*, which is not compatible with the managed String type. Fortunately, there is a function called PtrToStringChars that will convert the managed String into a wide character pointer. There is one small caveat in using this function. In order to ensure that the pointer will not be corrupted during garbage collection, you must pin the pointer.
During garbage collection, managed objects can potentially be moved to compact memory. The impact of this move is that the unmanaged pointer is pointed to a memory location that will become invalid. The __pin keyword ensures that this move does not happen in the first place.
One major problem with this technique is the behavior of the console when a user attempts to close it which can result in your application shutting down. To get around this, I disabled the ability to close the console window. Although this may sound trivial, I needed to do some tricks to make it happen—finding the window and deleting the close menu option from its system menu.
One final feature that I added to the console log was to display the console window only when there was output for that window. This involved opening and closing the window depending on what the current logging levels were set to. LogBase provides the facility to monitor what the current levels are and when they change. When there are no active logs, the console is closed. Alternatively, if the console is closed and a logging level is turned on, the console is opened. Other than the Close button and the String issue, developing the ConsoleLog class was straightforward. I used the Win32 console APIs to do everything I needed.

Logger
One of the goals of this project was to provide an extensible framework for logging. In order to do so, there has to be a way to initialize the existing logging types and add user-defined log types to the logger. I accomplished this by providing a base logger class that anyone can inherit from and a method that allows an instance of this class to be added to the list of known loggers.
The main logger class is responsible for managing the lifetime of the different logging mechanisms and routing the messages to all the existing loggers. Managing the lifetime of the existing loggers requires some coding by the client programmer. I have created a shutdown method to allow the program to tell the logging mechanism when things should be gracefully closed.
In order to manage the logging mechanisms, the Logger class must contain a list of the existing loggers. The .NET Framework provides several collection classes in System.Collection, which make the implementation of this list trivial. For the list of loggers, I decided on an ArrayList because it fit my needs and can easily be made thread safe. This list must be thread safe because any thread within an application may decide to add a new logging mechanism. To make a thread-safe version of an ArrayList, all you need to do is call the static method ArrayList.Synchronized.
Initialization can happen in many places within a program. I can either use a basic constructor, an Initialize method, or a static constructor. An initialization method works well as long as you remember to call it. A constructor has the advantage of being called by the .NET Framework. The .NET Framework also includes the concept of a static constructor, which is guaranteed to execute only once in the lifetime of a program, before the class is ever instantiated, and before any static members are initialized but after any static initializers. This ensures that the constructor will always be called exactly once if the object is used. This is great for the logging assembly since it allows singleton-like behavior without having to create a singleton.
Destruction is another interesting topic in the .NET Framework because the .NET Framework does not have the deterministic destruction that C++ developers are accustomed to using. In most instances, this isn't a problem, but I am creating threads and using resources that I will want to clean up properly, so I need to have control over when that shutdown takes place. In the logging framework, I created a method called StopLogger, which I expect the user to call before the application terminates. StopLogger uses a foreach loop to iterate through the list of loggers and call each one's StopLogger methods.
Register logger adds the logger to the list of known loggers and has the side effect of starting a thread for each logging type. It registers the Run method as the entry point for the thread and kicks it off with a call to ThreadStart. Once the thread is running, its priority is adjusted to run below the normal thread priority. This will help ensure that the logging mechanism does not significantly affect the performance of the running application. The thread methods are all part of the System.Threading namespace.

Conclusion
The concepts used in most object-oriented languages are not particularly difficult, but the implementation details often are. In writing the logging framework, most of my stumbling blocks arose from not being able to find system services that I have grown accustomed to having at my disposal. I have only scratched the surface of what is provided as part of the .NET Framework, but in doing so I hope I have provided a useful framework for you to use in your own projects.

For background information see:
GC Class
Static constructors
System.Reflection Namespace

Daryn Kielyis a Software Development Manager for International Game Technology in Las Vegas. Daryn is currently working on real-time data processing using C# and the .NET Framework. He can be contacted at kiely@lvcm.com.

Page view tracker