Enterprise Library

Take Exception To Critical Errors With Custom Application Blocks

Jay Hilyard

This article discusses:

  • An inside look at Enterprise Library
  • Building an error-reporting framework
  • Capturing and formatting exceptions
  • Logging, posting, and viewing error reports
This article uses the following technologies:
C#, .NET Framework, Enterprise Library

Code download available at:CriticalErrors2006_09.exe(4264 KB)

Contents

Inside Enterprise Library
Putting the Library to Work
Exception Formatting
Logging the Error
Posting the Error
Seeing the Results
Conclusion

I t seems like so many new technologies are coming out of Microsoft these days. They cover a huge range of ground, from SQL Server™ 2005 to new programming frameworks like the Microsoft® .NET Framework 2.0 and the upcoming .NET Framework 3.0. Among these new technologies is Enterprise Library, coming from the Microsoft patterns & practices group that will not only help you use best practices when creating your application, but may save you a lot of work as well. You may finally have time to put all those other technologies to work for your app as a result.

Enterprise Library is a collection of application functionality "blocks" that you can re-use in your application for common functionality you'd otherwise have to write again and again. These blocks are built as guidance for how to best use the technologies available in the .NET Framework 2.0. These application blocks also provide an extensibility model that allows for customization without having to change the code distributed by the Enterprise Library team. And for those cases where the extensibility model just doesn't cover enough ground, Enterprise Library is distributed as source code so modifications can be made for specific applications or companies.

There are application blocks for functionality in the areas of exception handling, logging, data access, caching, cryptography, and security (see Figure 1). Each block is highly configurable, which makes it easy for you to set up an installation of an application that uses the Enterprise Library. In this article, I will provide a brief overview of Enterprise Library's history and architecture, and then show you how to use Enterprise Library to help debug customer issues through a Critical Errors Reporting framework that can be plugged into any Windows® Forms-based application.

Figure 1 Enterprise Library Components and Dependencies

Figure 1** Enterprise Library Components and Dependencies **

Inside Enterprise Library

Enterprise Library began as a simple series of application blocks, the purpose of which was to help encapsulate common functionality found in enterprise applications as well as to provide guidance to the development community about how to use the .NET Framework. Enterprise Library was built using common design patterns and designed to be extensible, but also to maintain an ease-of-use that would make the blocks approachable by developers just learning about many of the challenges associated with enterprise-level applications.

In addition to the application blocks, Enterprise Library also shipped with source code, unit tests, quickstart applications that demonstrated the most common usage scenarios, and extensive documentation. Keeping with the idea of Enterprise Library as developer guidance material, the Enterprise Library team followed the best practice of developing unit tests for all of the component functionality. They did so using NUnit, one of the more common unit-testing frameworks, and they have since maintained the NUnit tests and added versions for Visual Studio® Team System. I can personally attest to these tests being quite helpful when learning a new section of the Enterprise Library; nothing clarifies a concept faster than being able to debug through the code and watch it tick.

The original Enterprise Library was released in January 2005 and was the first time a number of the application blocks had been packaged together as a library. A follow-up release was published in June 2005, addressing some issues from the January release and targeting the .NET Framework 1.1. In January 2006, a new version of Enterprise Library was released that took full advantage of new functionality added to the .NET Framework 2.0 and provided enhancements to the existing infrastructure. For example, the Logging Application Block was retooled to use TraceListeners instead of sinks as well as taking advantage of the new System.Configuration namespace.

Enterprise Library consists of a configuration tool, some design-time configuration elements, instrumentation elements, ObjectBuilder, and the individual application blocks. The configuration tool is the easiest way to add Enterprise Library functionality to your application and provides a user interface for changing the configuration settings that control how Enterprise Library works in the application. In the current version, it is also possible to use Enterprise Library without a configuration file, a big step forward from the initial releases. Enterprise Library is instrumented with various performance counters, Windows® Management Instrumentation (WMI) support, and event logging so that it can be monitored and supported more easily.

ObjectBuilder is a new component that is shared with the Composite UI Application Block (CAB), another piece of guidance code from the patterns & practices group that helps set up a smart client application infrastructure. You can think of ObjectBuilder as a more powerful and flexible version of Activator.CreateInstance because it uses a set of operations called a strategy to determine how an object is constructed. ObjectBuilder is based on the dependency injection pattern (see msdn.microsoft.com/msdnmag/issues/05/09/DesignPatterns for more information). This allows it to determine what components to use during the initial construction process of an object as specified in the object creation strategy. This strategy can be affected by the settings in the configuration file or by passing parameters directly to Enterprise Library.

The Caching Application Block applies to application situations where a local cache for objects increases performance. This is not meant to be a simple local cache, but rather a more advanced caching mechanism with configurable time windows, different backing storage for the cache, and cache tuning while the application runs. If you have a relatively small number of objects to cache, perhaps less than 100, you may be better off using a Dictionary<T>. As always, profile your code to see what works best for your application.

The Cryptography Application Block encapsulates the details of setting up and performing basic cryptographic functions. The two actions supported by the block are encrypting and decrypting data and generating hashes, both with a minimum of fuss. Even though the Cryptography Application Block simplifies cryptography dramatically, it still provides a high level of security.

The Data Access Application Block provides an easier way to implement the most common database interaction scenarios while taking advantage of the new classes in ADO.NET that help to abstract the database. The factory used to create database connections can be given the name of a connection string as a parameter, allowing for retargeting a database easily without code changes. The Data Access Application Block comes with providers for both Microsoft SQL Server and Oracle in the initial distribution and is extensible with other database providers that can be created and plugged into the block.

The Exception Handling Application Block provides a way for applications to centralize their error handling and reporting code as well as providing a mechanism to re-map exceptions as they cross a boundary in your application. For example, a SqlException from your data layer could be converted to an appropriate business layer exception that can be presented to users without them thinking something very bad happened to the database. The other great feature provided by the Exception Handling Application Block is an easy way to plug into the Logging Application Block to log exceptions depending upon the level of error being handled.

The Logging Application Block provides a common way to write information to a neutral stream and have it delivered in a number of different ways such as e-mail, an event log entry, a WMI event, or any other transport you can dream up. The Logging Application Block will also allow for including certain context information (Unmanaged Security, Managed Security, and COM+ context) when the information is logged.

The Security Application Block helps in the area of authentication via support for user session tokens and authorization through a generic approach to role-based security with pluggable authorization providers.

Enterprise Library has an active workspace on the GotDotNet Web site at practices.gotdotnet.com/projects/entlib. You can find blogs from the team, a user community where people contribute their own extensions to Enterprise Library, and links to webcasts that can help you to better understand the application blocks.

Putting the Library to Work

To demonstrate some of the cool things you can do with Enterprise Library through its base functionality and custom extensions, I'll show you one approach to tackling one of the age-old problems in software: debugging customer issues remotely. You have probably been in a situation where you wished you could have gotten more information when an error occurred at a customer site. My Critical Errors Reporting framework demonstrated here lets you add this capability to your own applications.

The Critical Errors Reporting framework takes advantage of four of Enterprise Library application blocks: Exception Handling, Logging, Data Access, and Cryptography. The Critical Errors Reporting framework creates a custom exception handler, a custom exception formatter, a custom LogFormatter, and a custom TraceListener to plug into Enterprise Library. It also uses the Data Access and Cryptography application blocks directly without any custom code to work with the error data and secure it during transport to the Web service.

The idea is that when exceptions occur in your application, you have a general idea which exception types are truly critical to your application's execution and health and which can be handled in a way that allows the application to continue. Using the Critical Errors Reporting framework in your application, you can monitor for those critical errors. When they occur, you capture lots of diagnostic and debugging information, post that information back to a Web service, and log that information into an issues database. I've provided a minimal reporting interface so that you can monitor new arrivals and investigate with the customer. If the customer cannot post to the Web service from your application, the issues are also logged locally to an XML-based file.

Playing the role of the badly behaved application being monitored in this demonstration is the aptly named Critical Error Producer. It also hosts a Web browser control to show the error posting result from the reporting site. The whole scenario is shown in Figure 2.

Figure 2 Critical Error Reporting Framework in Action

Figure 2** Critical Error Reporting Framework in Action **

The application would add the standard hooks for unhandled exceptions in Windows Forms applications: Application.ThreadException and AppDomain.CurrentDomain.UnhandledException. Both of those handlers would be routed to a single HandleException method. This is where the Critical Errors Reporting framework begins to take effect. The Enterprise Library Exception Block is used to report the exception by calling ExceptionPolicy.HandleException:

static void HandleException(Exception ex) { ExceptionPolicy.HandleException(ex, "Critical Errors Reporting Policy"); }

HandleException is a static method on the ExceptionPolicy class that instructs Enterprise Library to process this exception using the policy named Critical Errors Reporting Policy that has been set up using the Enterprise Library configuration tool. This policy tells Enterprise Library what types of exceptions to handle, how they should be handled, and the set of handlers to notify when the exception occurs. In this case, the policy has been set to handle the ObjectDisposedException and the DivideByZeroException and to route the exception handling to the CriticalErrorExceptionHandler, as shown in Figure 3.

Figure 3 Configuring a Custom Exception Handling Policy

Figure 3** Configuring a Custom Exception Handling Policy **

Each of the exceptions specified in the policy can in turn specify which handler processes the exception: Logging Handler, Replace Handler, Wrap Handler, or Custom Handler. As you'd expect, Logging Handler allows you to log an exception to an event log or file, while Replace Handler allows for transformation of an exception from one type to another. Replace Handler is useful in the case of a .NET Framework exception that should be transformed into a custom application exception to carry more of the application context along with it. Wrap Handler can be helpful to wrap business layer exceptions in suitable presentation layer exceptions with more appropriate messages for the user.

The Critical Errors Reporting framework takes advantage of the extensibility point provided by the Exception Handling Application Block in the form of Custom Handler. Both DivideByZeroException and ObjectDisposedException have Custom Handlers that are mapped to CriticalErrorExceptionHandler, which is a custom exception handler created as part of the Critical Errors Reporting framework (see Figure 4).

Figure 4 CriticalErrorExceptionHandler Class

[ConfigurationElementType(typeof(CustomHandlerData))] public class CriticalErrorExceptionHandler : IExceptionHandler { private static int _eventId = 0; private object _syncEventId = new object(); public CriticalErrorExceptionHandler(NameValueCollection ignore) {} private int GetNextEventId() { lock (_syncEventId) return _eventId++; } public Exception HandleException(Exception exception, Guid handlingInstanceId) { Guid issueTag = handlingInstanceId; string xmlExceptionData = string.Empty; using(MemoryStream memStream = new MemoryStream()) { CriticalErrorExceptionFormatter formatter = new CriticalErrorExceptionFormatter( memStream, exception); formatter.Format(); xmlExceptionData = Encoding.UTF8.GetString( memStream.ToArray()); } Dictionary<string, object> dictionary = new Dictionary<string, object>(); ManagedSecurityContextInformationProvider mgdInfoProvider = new ManagedSecurityContextInformationProvider(); mgdInfoProvider.PopulateDictionary(dictionary); UnmanagedSecurityContextInformationProvider unmgdInfoProvider = new UnmanagedSecurityContextInformationProvider(); unmgdInfoProvider.PopulateDictionary(dictionary); ComPlusInformationProvider complusInfoProvider = new ComPlusInformationProvider(); complusInfoProvider.PopulateDictionary(dictionary); LogEntry logEntry = new LogEntry(); logEntry.EventId = GetNextEventId(); logEntry.Severity = TraceEventType.Critical; logEntry.Priority = 2; logEntry.Message = xmlExceptionData; logEntry.Categories.Add("Critical Error Reporting Framework"); logEntry.ExtendedProperties = dictionary; logEntry.ExtendedProperties.Add("IssueTag", issueTag); NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); if (interfaces.Length > 0) { string macAddress = interfaces[0].GetPhysicalAddress().ToString(); logEntry.ExtendedProperties.Add("SiteCode", macAddress); } Logger.Write(logEntry); return exception; } }

To create this custom exception handler, a class was created that supports the IExceptionHandler interface. This is the interface the Enterprise Library Exception Handler block expects to communicate with for the various handlers when an exception is being handled:

public interface IExceptionHandler { Exception HandleException( Exception exception, Guid handlingInstanceId); }

HandleException takes two parameters, the exception that is being handled and a GUID identifier for the handling chain. It returns either a new exception or the original exception depending on the type of handler you build. The CriticalErrorExceptionHandler returns the unmodified exception as it is primarily used as a logging mechanism.

Exception Formatting

To control how the exception is formatted during the handling process, the Exception Handling Application Block provides the abstract base class ExceptionFormatter, which a custom formatter would inherit from to provide a different formatting implementation. The Critical Errors Reporting framework has a custom exception formatter named CriticalErrorExceptionFormatter. TextExceptionFormatter, the base formatter for the Enterprise Library, produces a text representation of the exception data, but CriticalErrorExceptionFormatter actually populates a DataSet with the information in the exception and then serializes the DataSet into a stream for transport.

The fundamental exception information is recorded (Type, Message, Source, HelpLink, and StackTrace) as well as the following types of information:

  • More comprehensive TargetSite information (the base formatter only gives the ToString version of the TargetSite)
  • The keys and values in the Exception.Data dictionary (the base formatter ignores these)
  • All property and fields values for public and private members
  • Common language runtime (CLR) and operating system version information
  • File version information
  • Process and environment information
  • Additional diagnostic information

All of this information is recorded to help give you the trail to follow when an exception occurs on the customer site and so you have a snapshot of what the process was dealing with when the exception occurred. The CriticalErrorExceptionFormatter code is shown in Figure 5.

Figure 5 CriticalErrorExceptionFormatter Definition

public class CriticalErrorExceptionFormatter : ExceptionFormatter { private CriticalErrorDS _errorDS = null; private MemoryStream _memStream = null; private CriticalErrorDS.ExceptionRow _exceptionRow = null; private static readonly ArrayList IgnoredProperties = new ArrayList(new string[] { "Source", "Message", "HelpLink", "InnerException", "StackTrace", "TargetSite", "Data" }); public CriticalErrorExceptionFormatter(MemoryStream memStream, Exception exception) : base(exception) { if (memStream == null) throw new ArgumentNullException("memStream", "Parameter ‘memStream’ cannot be null. " + "Please provide an instantiated MemoryStream instance."); if (exception == null) throw new ArgumentNullException("exception", "Parameter ‘exception’ cannot be null. " + "Please provide an exception to format."); _memStream = memStream; } public override void Format() { try { _errorDS = new CriticalErrorDS(); WriteException(base.Exception, Guid.NewGuid()); _errorDS.WriteXml(_memStream); } catch (Exception e) { Debug.WriteLine( "Formatting Exception threw exception: " + e); throw e; } } ... // Additional methods }

The last step before logging the application is to grab a bit of context for the exception. Context is everything in debugging a problem and the Exception Handling Application Block helps out here by providing three context providers that will get information specific to those contexts that is good information to know when debugging: ManagedSecurityContextInformationProvider, UnmanagedSecurityContextInformationProvider, and ComPlusInformationProvider.

As the names indicate, they provide information about the managed security context (identity, authentication type, and if the user is authenticated), the unmanaged security context (current user and the account the process is operating under), and the COM+ or Enterprise Services context (activity id, application id, transaction id, direct caller account, original caller account). I know I'd want this information when debugging security issues or transaction failures.

Logging the Error

Now that the exception has been formatted, it needs to be logged using the Logging Application Block and the first step in doing that is to create a LogEntry and submit it using the static Logging.Write method. The LogEntry has a number of properties to set, some of which help control the routing of the LogEntry in the logging system. For this sample, I will be submitting exceptions that are being handled by the CriticalErrorExceptionHandler with a LogEntry.Severity of TraceEventType.Critical.TraceEventType is in the System.Diagnostics namespace and represents the type of event being logged for TraceListeners. The different types of trace events, along with a brief description, are listed in Figure 6.

Figure 6 TraceEventType Enumeration Values

Event Description
Critical Fatal error or application crash
Error Recoverable error
Information Informational message
Resume Resumption of a logical operation
Start Starting of a logical operation
Stop Stopping of a logical operation
Suspend Suspension of a logical operation
Transfer Changing of correlation identity
Verbose Debugging trace
Warning Noncritical problem

For the LogEntry.Message field, I will add the serialized exception data from the custom exception formatter and add a LogEntry.Category of "Critical Error Reporting Framework" to identify this log entry to the system. This is important as the categories are one of the pieces of data used to route the log entry to the proper TraceListener.

The Logging Application Block provides a number of TraceListeners that allow you to route log entries via e-mail, Microsoft Message Queuing (MSMQ) or WMI and store them in a database, a flat file, or the event log. There is also a System.Diagnostics TraceListener node in the Enterprise Library design-time configuration tool for using a predefined TraceListener that was built to work with the tracing capacity in the .NET Framework. Finally, there is a custom TraceListener that can be configured if none of the previous options suits your needs. In this case, the TraceListener implemented by the Critical Errors Reporting framework is a custom TraceListener that posts the log entry to a Web service so the errors can been seen that are occurring in the field. The custom TraceListener configuration is shown in Figure 7.

Figure 7 Configuration of CriticalErrorTraceListener

Figure 7** Configuration of CriticalErrorTraceListener **

The CriticalErrorTraceListener is a custom implementation of a TraceListener that will take any log entries it receives and post them to the Web service. This TraceListener will also write to a local issue log file so that the errors are not lost if connectivity cannot be established with the Web service.

The single attribute that can be specified for the CriticalErrorTraceListener is called issueLogPath and it specifies where on the local system the local issue log should be created and written to. To set the attribute, click into the Attributes field for the Custom Trace Listener entry and click on the ellipsis (...) button to see the attribute configuration window as shown in Figure 8.

Figure 8 Attribute Configuration for the TraceListener

Figure 8** Attribute Configuration for the TraceListener **

The Logging Application Block also allows the use of one of the provided logging filters or for the use of a custom logging filter. These filters determine if the log entry should be routed to the configured TraceListeners or screened out entirely. Filtering occurs before any of the TraceListeners is contacted with the entry. The ability to provide a custom filter is a new feature in Enterprise Library and opens up the possibility for filtering past the priority and category stock filters. One other filter of note is the LogEnabled, which provides a way to disable or enable all logging in a single location without adding or removing existing configuration information.

Once the issue log path has been set, the LogEntry needs to have a formatter to create the data to be logged. The ever-extensible Enterprise Library comes to the rescue again as a custom formatter can be provided for just such a purpose. While formatting of the LogEntry, additional information is added, such as an issue tag for identification, a site code to identify where the error occurred, the categories for the log entry, and any extended properties that were set up on the entry.

The custom formatter for log entries that has been created as part of the Critical Errors Reporting framework is DataSetXmlLogFormatter. As the name indicates, it formats the LogEntry into a DataSet and then extracts the serialized XML from the DataSet for transport in the Format method shown in Figure 9. This may seem like a roundabout way to get an XML representation of the LogEntry, but consider this: the XML data is going to be turned back into a DataSet on the other end of the wire so this makes the entire process less difficult. Adding the rows to the tables in the DataSet initially saves the coding effort of turning them into XML and then back into a format that is ready to be persisted to a database through the magic of ADO.NET. I am not one to shy away from XML, but why do work the framework is happy to do for you?

Figure 9 Turning a LogEntry into Serialized XML

public override string Format(LogEntry logEntry) { string xmlLogEntry = ""; try { _errorDS = new CriticalErrorDS(); using (StringReader exceptionStream = new StringReader(logEntry.Message)) { XmlReadMode mode = _errorDS.ReadXml(exceptionStream); } CriticalErrorDS.LogEntryRow row = errorDS.LogEntry.NewLogEntryRow(); row.id = Guid.NewGuid(); WriteSiteCode(row, logEntry); WriteIssueTag(row, logEntry); row.activityId = logEntry.ActivityId; row.appDomainName = logEntry.AppDomainName; row.errorMessages = logEntry.ErrorMessages; row.eventId = logEntry.EventId; row.categoriesId = Guid.NewGuid(); WriteCategories(logEntry.CategoriesStrings, row.categoriesId); WriteExceptionId(row); row.extendedPropertiesId = Guid.NewGuid(); WriteExtendedProps(logEntry.ExtendedProperties, row.extendedPropertiesId); errorDS.LogEntry.Rows.Add(row); using (MemoryStream memStream = new MemoryStream()) { _errorDS.WriteXml(memStream); xmlLogEntry = Encoding.UTF8.GetString(memStream.ToArray()); } } catch (Exception e) { Debug.WriteLine("Formatting LogEntry threw exception: " + e); throw e; } return xmlLogEntry; }

Posting the Error

Now that the log entry has been formatted into an XML stream, I need to send it to the Web service. To protect the information in transit, the CriticalErrorTraceListener.FormatAndLog method uses the Cryptography Application Block to encrypt the stream. Figure 10 illustrates this as well as the call to write the entry to the local issue log.

Figure 10 Log Entry Formatting and Encryption

private void FormatAndLog(LogEntry logEntry) { string entryMessage = this.Formatter.Format(logEntry); WriteLocalEntry(entryMessage); byte[] entryBytes = Encoding.Unicode.GetBytes(entryMessage); byte[] encryptedEntryBytes = Cryptographer.EncryptSymmetric( "RijndaelManaged", entryBytes); string encodedEncryptedEntryMessage = Convert.ToBase64String(encryptedEntryBytes); byte[] encodedEncryptedEntryBytes = Encoding.UTF8.GetBytes(encodedEncryptedEntryMessage); CriticalErrorReportingTarget.CriticalErrorReportingService reportService = new CriticalErrorReporting. CriticalErrorReportingTarget.CriticalErrorReportingService(); reportService.ReportCriticalError(encodedEncryptedEntryBytes); }

The Cryptography Application Block provides the Cryptographer class that has the static methods EncryptSymmetric and DecryptSymmetric as easy ways to encrypt and decrypt data. The encryption algorithm is controlled by the provider name ("RijndaelManaged") that corresponds to settings in the configuration file specifying the type of symmetric algorithm to use.

The Key field in the configuration holds the encryption key. This key is stored in the configuration file but is encrypted based on either user or machine mode encryption using the Data Protection API (DPAPI). If you employ user mode to encrypt the key, the credentials of the currently logged in user are used to encrypt the key, while if you select machine mode, the key is encrypted using machine specific information so that any user logged in can access the key.

The key management facilities allow you to create a key, use a DPAPI-encrypted key file, or import a password-protected key file. Once a key has been established using the Enterprise Library configuration tool, you can right-click on the symmetric provider node to export the key to a password-protected file.

Once the log entry data has been secured, it is time to post it to the Web service. CriticalErrorReportingTarget is the Web reference that the application holds to the CriticalErrorReportingService. This would be the URL pointing to where the Web service is set up at some remote location. CriticalErrorReportingService has one method, ReportCriticalError, which takes an array of bytes, XML that has been encrypted and Base64-encoded. ReportCriticalError is responsible for processing the log entry into the remote database. This particular Web service was created to emphasize Enterprise Library, but it could easily be redone using Windows Communication Framework or another technology.

After CriticalErrorTraceListener posts the log entry to CriticalErrorReportingService, the Web service reverses the process by decrypting the byte stream, Base64 decoding it, and turning it back into a string for processing. To decrypt the log entry, there is a set of configuration entries for the Cryptography and Data Access Application Blocks being used in the Web service. The same Symmetric Provider is set up for the Cryptography Application Block and the key has been imported from a password protected file so that it matches the key used by the CriticalErrorTraceListener to encrypt the stream coming from the client. If the keys do not match, the payload will not be decrypted properly and the entry will not be added to the database.

The Data Access Application Block uses the Enterprise Library configuration to establish a connection string (shown as CriticalErrorConnectionString) for a SQL Express database built to hold the data from the errors. Once the log entry stream is ready for processing, a database connection is established using the Enterprise Library:

Database db = DatabaseFactory.CreateDatabase( "CriticalErrorConnectionString");

When the connection is established, the log entry can be persisted to the database. The log entry is reloaded into a DataSet instance and then the Data Access Application Block is used to insert the corresponding records into the database. The Data Access Application Block takes advantage of the common database interaction classes and interfaces defined in System.Data.Common, which helps to hide what database is being accessed. This is useful in scenarios where your application needs to be able to support multiple databases without changing code as the database provider can be changed in the configuration. To actually insert the records, the Database entity (with the connection string information) is first used to create a DbConnection object that is our connection to the database. Once the DbConnection has been established, a transaction is started using the DbConnection.BeginTransaction instance method for that connection, which returns a DbTransaction object. The log entry persistence should be an atomic action, which is why a transaction is used.

Now that the transaction is set up, the parts of the log entry can be persisted to the database. The Database.UpdateDataSet instance method takes the DataSet, a table name, and the appropriate insert, update, and delete commands as parameters and performs the update:

LogEntryTableAdapter adapter = new LogEntryTableAdapter(); adapter.Connection = (SqlConnection)connection; db.UpdateDataSet(this, "LogEntry", adapter.GetInsertCommand(), adapter.GetDeleteCommand(), adapter.GetUpdateCommand(), transaction);

The insert, update, and delete commands are instances of DBCommand objects that represent the respective operations. The Data Access Application Block normally requires the creation of these commands from scratch. However, it seemed like a waste to recreate all of these statements when the TypedDataSet for the Critical Errors Reporting framework database provides a nice visual way to generate these statements and parameters, which it places in the TableAdapters provided for each table in the DataSet. Unfortunately the TableAdapters don't expose the command collection as public members. This can be worked around, however, as the TableAdapters are partial classes in the generated code. By implementing a small partial class extension to each of the TableAdapters, you can then access the commands and not have to maintain these queries using two different mechanisms. Since this part of the partial class definition has access to the internal command collection, I can expose those commands as shown here:

public partial class LogEntryTableAdapter { public System.Data.SqlClient.SqlCommand GetInsertCommand() { return this.Adapter.InsertCommand; } public System.Data.SqlClient.SqlCommand GetUpdateCommand() { return this.Adapter.UpdateCommand; } public System.Data.SqlClient.SqlCommand GetDeleteCommand() { return this.Adapter.DeleteCommand; } }

Now I can use the commands on the TableAdapters in the UpdateDataSet call once I have set the connection to be the DbConnection created earlier. This also enlists the TableAdapter in the transaction so the commands are part of the overall transaction. Each of the tables in the DataSet have this TableAdapter partial class extension so the same pattern is followed to save the data for the rest of the DataSet. If the updates complete successfully, the transaction is committed and the processing is complete. If any of the updates fail, the transaction is rolled back and the entry is not persisted to the database. The use of the TableAdapters in this fashion is not necessary as DBCommand instances could have been created individually for each of the tables using the Data Access Application Block. The TableAdapter approach was used here to save time and to demonstrate a possible usage of the DataSet related classes since they are declared as partial classes.

Seeing the Results

Once the log entry has been saved to the database, the application vendor has access to the data through a rudimentary Web site. This initial page lists the currently logged errors and a brief description of the errors, where they came from, and when they occurred. Clicking on the Issue Tag hyperlink will then bring up the detail information for the error, as shown in Figure 11.

Figure 11 Viewing Errors Reported from a Cusomer Site

Figure 11** Viewing Errors Reported from a Cusomer Site **

There are a number of categories of information that are recorded. The categories that had information reported have an active link at the top of the page as certain categories of information won't apply to all errors. The specific categories of information are listed in Figure 12.

Figure 12 Critical Error Information Categories

Category Description
Exception Details The main detail of the exception found directly on the exception at run time.
Exception Data The keys and values found in the Exception.Data collection.
Exception Properties All property values for the given exception.
Exception Fields All field values for the given exception.
Inner Exception Information All of the inner exceptions for the main exception.
Target Site Information The detailed target site information containing the type and metadata information.
Target Site Generic Arguments Information about any generic type arguments the target site may have.
Additional Exception Information The AppDomain, machine name, exception timestamp, and identity information.
Diagnostic Information The CLR version, the executing assembly, and the version information.
Host Characteristics Operating system, processor count, and the current directory for the application.
Process Information Process identifiers, processor and memory utilization statistics, and processor time.
Process Environment Variables The process (system) environment variables.
Process Start Information The arguments, file name, and if the shell was used to launch the process.
Process Start Information Environment Variables The environment variables accessible to the identity (user) running the process.
Process Start Information Verbs The verbs associated with this type of file.

All of this information is also retrieved by the Critical Error Producer and displayed in the WebBrowser control to show that the error was logged to the Web service successfully. This may or may not be appropriate for your application but is included to demonstrate the capacity.

The local issue log is accessed in the location specified in the configuration for the TraceListener, but if the Show Local Log button is clicked in the Critical Error Producer, you can see it in your associated XML viewer. The XML format of the log file is produced from the DataSet and wrapped in a root tag of CriticalErrorEntries.

Conclusion

You can learn a great deal about better ways to create, manage, secure, and deploy your applications by spending a bit of time perusing the various projects, webcasts, white papers, and other content produced by the patterns & practices group. They can save you time, effort, and sanity in dealing with those technologies. They have projects covering Web services, security, enterprise application development (Enterprise Library), smart client development, performance, manageability, and many other things as well. If you have any interest in writing better code, building a better foundation for your product set, or learning more about application architecture with the .NET Framework, do yourself a favor and check them out at msdn.microsoft.com/practices.

Enterprise Library is a set of code that makes building enterprise-level applications easier as it provides a fair amount of the plumbing code common to this type of application. One of the important things to realize is that you don't have to implement all of the blocks in the Enterprise Library to realize benefits in your code base. You can use the parts that make sense for your application and leave the rest until you have time to investigate further or not use them at all. The blocks are meant to allow you to concentrate on the good stuff in your application. The good stuff is the business logic you know best that when done right, makes your customers sing your praises, helps your company thrive, and lets your personal satisfaction grow. You can learn more about the Enterprise Library and download the deliverable from msdn.microsoft.com/library/en-us/dnpag2/html/EntLib2.asp.

Jay Hilyard is the co-author of the C# Cookbook (O’Reilly, 2nd edition) and currently works on the New Product Development team at Newmarket International. Thanks to Tom Hollander and the Enterprise Library team for their input on this article.