Design of the Configuration Application Block Runtime


There are two parts to the Configuration Application Block: the runtime and the design-time (the design and features of the design-time are covered in the next chapter). The runtime component focuses on simplifying the reading and writing of configuration data, which allows applications to be loosely coupled with their configuration data stores, and providing a model that allows a developer to extend the capabilities of the block for situations where the Configuration Application Block doesn't perfectly fit an enterprise's needs (e.g., the need to store configuration data in a relational database other than Microsoft SQL Server).

Reading and Writing Configuration Data

The first step to being able to read and write configuration data in an application is to determine the in-memory representation of that data. Without that, nothing else matters. This representation might be as simple as a string or as complex as an object graph.

For the Configuration Application Block to read and write this data, it must be able to represent the data in a serial format. The Configuration Application Block requires that an object representing configuration information supports XML serialization. This way, the data in an object that is described as public classes, fields, properties, primitive types, arrays, and embedded XML in the form of XmlElement or XmlAttribute objects will get serialized. When the Configuration Application Block saves the state for such an object, it uses the XmlSerializer to save the public properties and fields as XML. When it recreates the object, it deserializes it from this XML.

An example might help illustrate this point. Imagine that you have a simple Windows application that can contain numerous tabs; each tab has different background colors, labels, and data in it. The number of tabs in the application, label for the tags, background color, and information on the tab are all configurable. To represent this information, a TabConfig class can be defined that contains an array of ApplicationTab objects. An ApplicationTab object contains information like the name of the tab, the background color, and a string to display on the tab. Listing 1.1 shows what the ApplicationTab class might look like so that it supports XML serialization.

Listing 1.1. ApplicationTab Class Sample

[C#] /// <summary> /// Represents the information for a tab. /// </summary> public class ApplicationTab {      private Color tabColor;      private string label;      private string message; /// <summary> /// Default constructor /// </summary> public ApplicationTab() {} /// <summary> /// Don't save this attribute. /// Provided to facilitate getting/setting /// background color of tab through code. /// </summary>  [XmlIgnore] public Color TabColor {      get      {           return this.tabColor;      }      set      {           if (! value.IsEmpty)                this.tabColor = value;      } } /// <summary> /// Used to serialize the color for the tab. /// Will be serialized with tag of TabColor. /// </summary>  [XmlElement("TabColor")] public string BackColor {        get        {            ColorConverter colorConverter = new ColorConverter();            return colorConverter.ConvertToString(TabColor);        }        set        {            if (value != null)            {                 ColorConverter colorConverter =                      new ColorConverter();                      TabColor =                      (Color)colorConverter.ConvertFromString(value);            }        }     } ... } [Visual Basic] Public Class ApplicationTab      Private myTabColor As Color      Private myLabel As String      Private message As String      ' Default constructor      Public Sub New()      End Sub      ' Don't save this attribute.      ' Provided to facilitate getting/setting      ' background color of tab through code.      <XmlIgnore> _      Public Property TabColor() As Color           Get                Return Me.myTabColor           End Get           Set                If (Not Value.IsEmpty) Then                     Me.myTabColor = Value                End If           End Set      End Property      ' Used to serialize the color for the tab.      ' Will be serialized with tag of TabColor.      <XmlElement("TabColor")> _      Public Property BackColor() As String           Get                Dim colorConverter As ColorConverter = _                     New ColorConverter()                Return colorConverter.ConvertToString(TabColor)           End Get           Set                If Not Value Is Nothing Then                     Dim colorConverter As ColorConverter = _                          New ColorConverter()                     TabColor = CType(colorConverter.ConvertFromString _                            (Value), _ Color)                End If           End Set      End Property ... End Class

All public fields and properties will be serialized except for those that are explicitly marked with the XmlIgnore attribute. In Listing 1.1, the TabColor property will not be serialized because it is attributed with XmlIgnore, but the BackColor property will be serialized as a TabColor element because it is attributed as XmlElement("TabColor").

Other public properties, like Label and Message, which are not shown in Listing 1.1, will just be serialized with the name of that property because they are not attributed in any way. The TabConfig class contains a public property that allows the entire array of ApplicationTab objects to be serialized and deserialized. The complete source code for this chapter is on the book's Web site. When this application uses the Configuration Application Block to save its configuration data, it might look Listing 1.2.

Listing 1.2. Sample Configuration for the ApplicationTab Class

<?xml version="1.0" encoding="utf-8"?> <TabConfig>   <xmlSerializerSection type="Defining_Config_Info.TabConfig, Defining       Config Info, Version=1.0.2119.14112, Culture=neutral,       PublicKeyToken=null">     <TabConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema"            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">       <Tabs>         <ApplicationTab>           <TabColor>Green</TabColor>           <Label>Tab1</Label>           <Message>This is Tab1</Message>         </ApplicationTab>         <ApplicationTab>           <TabColor />           <Label>Tab Numero Duo</Label>           <Message>This is Tab2</Message>         </ApplicationTab>         <ApplicationTab>           <TabColor />           <Label>Tab3</Label>           <Message>This is the third tab</Message>         </ApplicationTab>       </Tabs>     </TabConfig>   </xmlSerializerSection> </TabConfig>

Decoupling Physical Storage Location

Although I have described how the configuration data needs to be represented in order to be serialized and deserialized by the Configuration Application Block, I have not mentioned anything about where it gets stored. That is because a developer does not need to know anything about location for the configuration data. By using the Configuration Application Block, determining where configuration data gets stored is more of an operational issue than it is a development decision.

The Configuration Application Block uses a different set of configuration data, known as configuration metadata or metaconfiguration, which tells it where to store the application configuration data. The metaconfiguration is always stored in the application domain configuration file (i.e., app.config or web.config), so the Configuration Application Block always has one place to go to read it. The application configuration data, like that shown in Listing 1.2, can be placed in a separate data store, separated from the metaconfiguration.

While this feature may seem trivial at first, it is actually quite powerful. The application can be completely ignorant about the physical location of its configuration data. It does not need to know whether the configuration data is stored in a file, the Windows Registry, a relational database, or some other type of data store. Thus, you don't need to write code to store configuration data in one of those particular data stores. At a later date, if the personnel responsible for the operations and maintenance of the application decide to change the location for where the configuration data should be stored, they merely have to modify the metaconfiguration for the application; no code needs to change.

StorageProviders and Transformers are the keys to insulating applications from the physical storage location and providing an extensible model that allows for custom storage locations (see Figure 1.1).

Figure 1.1. Reading and Writing Configuration Objects


  • A StorageProvider allows an application to have its metaconfiguration stored in a data store other than the one that houses the application data.

  • The IStorageProviderReader interface defines the interface that is used to read configuration information from a storage location.

  • The IStorageProviderWriter interface defines the interface that is used to write configuration information.

  • The ITransformer interface transforms configuration setting objects between the application and the StorageProvider.

Enterprise Library ships with four StorageProviders: the XmlFileStorageProvider, the AppConfigFileStorageProvider, the RegistryStorageProvider, and the SqlStorageProvider. As the names imply, these are used for reading and writing configuration data from and to XML files (other than the application domain configuration file), the application configuration file, the Windows Registry, and Microsoft SQL Server respectively.

StorageProviders have just two responsibilities: reading configuration data from a particular data source and writing data to a particular data source. The XmlFileStorageProvider, for example, points to a file that contains particular configuration settings and accepts and returns objects of type XmlNode. The design behind the other StorageProviders is the same except that RegistryStorageProvider has code that reads and writes to Registry keys, and SqlStorageProvider uses database connections, commands, and stored procedures for reading and writing to Microsoft SQL Server.

I mentioned earlier that configuration data must be serialized and deserialized. This job, however, is not the responsibility of a StorageProvider. Once the data is read or written, the StorageProvider's job is complete. Serializing and deserializing the configuration data is the job of a Transformer. Enterprise Library ships with an XmlSerializerTransformer. This Transformer uses an XmlSerializer to serialize and deserialize the data to a form that can be used by the application or process. By default, all the Enterprise Library application blocks leverage the XmlFileStorageProvider and XmlSerializerTransformer for reading and writing configuration data from and to XML files. Figure 1.2 shows how an XmlSerializerTransformer and an XmlFileStorageProvider can be combined to serialize and deserialize configuration data to and from an XML format and to read and write that configuration data to an XML file.

Figure 1.2. Reading and Writing to XML Files


When you save configuration data using the XmlSerializerTransformer, the XmlSerializer is used to serialize the configuration data to an XmlNode object. If you also use the XmlFileStorageProvider, then the XML is written to the predefined file. When the configuration data is read, the reverse process happens: the XmlFileStorageProvider reads the data from the XML file and returns an XmlNode object. Then the XmlSerializerTransformer deserializes the data and returns it to the application block, which then returns it to the application. If the RegistryStorageProvider is used instead of the XmlFileStorageProvider, the data is read and written using the Registry. Using a specific Transformer does not necessitate using a particular StorageProvider; the two are distinct from one another and can be assembled in any combination.

Providers in Enterprise Library

The ability to decouple the configuration data from a specific data store or format is not a result of a specific implementation for a StorageProvider or Transformer. It is a result of the Provider Model Design Pattern that exists in the Configuration Application Block. This pattern allows an easy-to-understand interface to be used while also providing complete control over the internals of what occurs when an implementation of that interface is called. A provider is a contract between an interface and an application or process that consumes that interface. The provider is the implementation of the interface separated from the interface itself. [1]

[1] Summarized from Rob Howard's Provider Design Pattern, Part 1 and 2 at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspnet/html/asp04212004.asp

There are some common characteristics for providers. A concrete, provider implementation should derive from an abstract base class or interface that is used to define a contract for a particular feature. The XmlFileStorageProvider, for example, derives from the StorageProvider base class. This base class in turn typically derives from a common provider base class. In Enterprise Library, all providers either directly or indirectly implement the IConfigurationProvider interface. This provider base class is used to mark implementers as a provider and forces the implementation of a required method and property common to all providers.

For example, when configuration data is read, the Configuration Application Block uses its metaconfiguration data to determine which implementation of the IStorageProviderReader interface it should instantiate; when configuration data is written, the Configuration Application Block uses its metaconfiguration data to determine which implementation of the IStorageProviderWriter to instantiate. Since applications typically read data from the same data store to which they write data, the Configuration Application Block only allows one StorageProvider for both operations. The Configuration Application Block is not written to know anything about a specific implementation for a StorageProvider, though; it is written to use the StorageProvider interface. Through its metaconfiguration, it determines which instance of the provider to use at runtime.

A closer look at the design for StorageProviders and Transformers can help you understand how this pattern is used in the Configuration Application Block and throughout all the Enterprise Library application blocks.

The IConfigurationProvider Interface and the ConfigurationProvider Class

Enterprise Library follows the Provider Model Design Pattern by requiring that all providers implement the IConfigurationProvider interface in order for the Configuration Application Block to properly create them. The IConfigurationProvider interface is very straightforwardit consists of a property called ConfigurationName and a method named Initialize. The Configuration Application Block also includes an abstract base class named ConfigurationProvider that implements the ConfigurationName property of the IConfigurationProvider interface. This is included to make it easier to create providers. Thus, providers do not need to explicitly implement the IConfigurationProvider interface, but can instead derive from the ConfigurationProvider base class and implement the Initialize method. This method initializes the provider to the correct state and context used by the factory creating it.

The Initialize method accepts an object representing the current ConfigurationView and uses it to obtain the provider's runtime configuration data. The ConfigurationView base class signifies a wrapper around a specific type of configuration information. For example, a StorageProvider expects the ConfigurationView to be of type RunTimeConfigurationView because it holds the metaconfiguration data which has, among other things, configuration information specific to a configuration data store (e.g., information about the name of the file for an XmlFileStorageProvider). In the Data Access Application Block, a database provider expects a DatabaseConfigurationView because a DatabaseConfigurationView contains configuration data specific to that type of database provider (e.g., connection information for connecting to Microsoft SQL Server).

The ConfigurationView is very important because it isolates a provider from having to know anything about how to retrieve the data that is needed. This lets an application that uses these providers respond to changes in the underlying configuration data store. If an XmlFileStorageProvider is used because configuration data is held in an XML file, and that XML file is modified outside of the application, the configuration runtime is made aware of this fact and the new configuration data can be retrieved and used. This is extremely effective, as it also allows configuration data to be cached by the Configuration Application Block without concern for that data becoming stale.

It is also important to recognize that the configuration runtime is caching the configuration data and therefore there is no need (and it is not recommended) to cache references to this data yourself. How, exactly, the Configuration Application Block is able to recognize and respond to a change that has occurred in a particular configuration data store is an implementation detail in a specific StorageProvider.

The IStorageProviderReader and IConfigurationChange-WatcherFactory Interfaces and the StorageProvider Class

To read configuration data from a particular data store, a StorageProvider must implement the IStorageProviderReader interface. As such, any implementation of an IStorageProviderReader must implement the Initialize method so that the Configuration Application Block can initialize the StorageProvider to its correct state and context.

In addition to inheriting from the IConfigurationProvider interface, the IStorageProviderReader interface also inherits from the IConfigurationChangeWatcherFactory interface. This allows the configuration runtime to recognize and react to changes that occur in the StorageProvider's data source. IConfigurationCacheWatcherFactory defines a single method: CreateConfigurationChangeWatcher.

Since the IStorageProviderReader interface implements the IConfigurationChangeWatcherFactory interface, any class that implements IStorageProviderReader must also have a CreateConfigurationChangeWatcher method. This method must return an IConfigurationChangeWatcher, which provides a StorageProviderReader with the ability to watch for changes in a particular configuration data store. Unfortunately, even if this functionality is not needed, it is still required that a StorageProviderReader return an IConfigurationChangeWatcher.

In this scenario, a ChangeWatcher that does nothing must be created and returned. Scott Densmore, the lead software development engineer for Enterprise Library, has written an example of a ChangeWatcher that does nothing and that can be used in these situations. For more information on this ChangeWatcher, see Scott's blog at http://blogs.msdn.com/scottdensmore. Creating an IConfigurationWatcher that actually does do something is the most difficult task in creating a StorageProviderReader. This is true for several reasons, which are discussed later in this chapter.

Similar to the ConfigurationProvider, the Configuration Application Block supplies an abstract StorageProvider [2] base class to make creating StorageProviderReaders easy. To create a StorageProviderReader, you only need to derive from the StorageProvider class and implement the Initialize, CreateConfigurationChangeWatcher, and Read methods. Table 1.1 lists the methods and properties that a derived StorageProvider class needs to implement to support the IStorage- ProviderReader interface.

[2] Long before it was released, the StorageProvider used to implement both the IStorageProviderWriter and IStorageProviderReader interfaces. It was changed shortly before release to only implement the IStorageProviderReader interface to support StorageProviders that only need read-only data. The 'Reader' suffix, however, was never added on to the name for this class.

Table 1.1. Implementation Requirements to Support the IStorageProviderReader Interface

Method/Properties

Description

SectionName

Gets or sets the configuration section name for which the storage is configured.

CreateConfigurationChangeWatcher

Creates an object that is responsible for watching for changes in the underlying storage mechanism for configuration persistence. When a change occurs, this object must raise its ConfigurationChange event.

Read

Reads the configuration from storage.


The IStorageProviderWriter Interface

The IStorageProviderWriter interface represents a StorageProvider writer for configuration data. Only the Write method needs to be implemented to support the IStorageProviderWriter. The Configuration Application Block calls the IStorageProviderWriter's Write method to save configuration data to storage.

Enterprise Library StorageProviders

Enterprise Library ships with four StorageProviders: XmlFileStorageProvider, AppConfigFileStorageProvider, RegistryStorageProvider, and SqlStorageProvider. All of these StorageProviders inherit from the StorageProvider base class and implement the IStorageProviderWriter, as illustrated in Figure 1.3. Each StorageProvider has been created to read and write configuration data from a different data store.

Figure 1.3. StorageProviders in Enterprise Library


  • The XmlFileStorageProvider reads and writes configuration data from and to the XML files.

  • The AppConfigFileStorageProvider represents a StorageProvider to read and write data to an application domain configuration file (i.e., app.config or web.config). This allows the metaconfiguration and configuration data to exist in one file, since the metaconfiguration data will always reside in the application domain configuration file.

  • The RegistryStorageProvider reads and writes configuration data from and to the Windows Registry. The RegistryStorageProvider existed in the previous Configuration Block and was introduced to Enterprise Library shortly after its initial release. As such, the design goals and implementation details are the same as they were in the previous version of the Configuration Block. For security reasons, only the Current_User, Local_Machine, and Users registry hives are supported

  • The SqlStorageProvider reads and writes configuration data from and to a Microsoft SQL Server database table (the Configuration_ Parameters table). A trigger exists on this table that updates information to be used for detecting changes to the configuration data that occur outside of a particular application. Like the RegistryStorageProvider, the SqlStorageProvider existed in the previous Configuration Block and was introduced to Enterprise Library shortly after its initial release.

A Custom StorageProvider: DatabaseStorageProvider

When Enterprise Library was initially released, it only shipped with the XmlFileStorageProvider. If you wanted to store configuration data somewhere besides XML files, there were few choices but to create your own custom StorageProvider. Soon after the initial release, the Enterprise Library Team released a patch that included several extensions. Three of these are the AppConfigFileStorageProvider, RegistryStorageProvider, and the SqlStorageProvider.

Still, these additional providers don't negate the possible need for a custom StorageProvider. What if an enterprise has standardized on a relational database technology other than Microsoft SQL Server yet prefers to have configuration data stored in a database? Are they left with having to choose between using Enterprise Library and their standard relational database?

The answer is no. Recall that subscribing to the Provider Model Design Pattern only exposes an application to the interface for a provider; an application is not bound to a particular implementation of a provider. If a different provider is needed to meet a particular circumstance, it can be developed and used without any code modifications to an application.

This section walks you through the details for creating a custom StorageProvider. This provider uses the Data Access Application Block for abstracting away the details involved with connecting to different types of relational database engines. (The Data Access Application Block is covered in Chapter 3, so you may not understand all of its features. At this point, it is only necessary to understand one of the primary concepts behind the Data Access Application Block: subscribing to the Provider Model Design Pattern allows an application or service to abstract away the details with connecting to a particular data store.) I leverage this feature to enhance the SqlStorageProvider. Since this new StorageProvider will no longer be specific to Microsoft SQL Server, I've named it DatabaseStorageProvider.

There are five steps to create this custom StorageProvider and one step to configure it.

1.

Set up the database (specific to this StorageProvider).

2.

Create a class that acts as the data transfer object for the provider. The sole purpose for this class is to represent the data needed for a particular provider.

3.

Create the ConfigurationChangedEventArgs and ConfigurationChangingEventArgs classes that are specific to this StorageProvider.

4.

Create a ConfigurationChangeWatcher class that is specific to the StorageProvider.

5.

Create the StorageProvider.

6.

Configure the new StorageProvider for an application.

Figure 1.4 depicts steps 25.

Figure 1.4. Design for the DatabaseStorageProvider


Step 1: Set Up the Database

This step is not needed for every custom StorageProvider, but it is necessary for this one. A modified SQL script from the one that is used for Enterprise Library's SQLStorageProvider can be used to create the database for this StorageProvider. This script makes it easy to extend a database with a table for storing an application's configuration data, stored procedures for retrieving and storing this data, and a trigger that will update a DateTime field to signify that the configuration data has been modified. To use a database other than Microsoft SQL Server, you will need to modify this script.

Step 2: Create the Data Transfer Object

The first task is to create a data transfer object to represent the information needed for reading and writing configuration data from and to the database. To represent this information, I have encapsulated three strings: one for holding the name of the database instance, one for holding the name of the stored procedure that will retrieve the configuration data, and one for holding the name of the stored procedure that will set the configuration data.

The DatabaseStorageProviderData class is derived from the abstract StorageProviderData class, as shown in Figure 1.4. In this model, it is the StorageProviderData classes that actually maintain the information about their respective StorageProvider. The DatabaseStorageProviderData class is a serializable class that contains properties for getting and setting the three strings mentioned in the previous paragraph. Listing 1.3 shows the code for the DatabaseStorageProviderData.

Listing 1.3. DatabaseStorageProviderData Class

[C#] [XmlRoot("storageProvider",      Namespace=ConfigurationSettings.ConfigurationNamespace)] public class DatabaseStorageProviderData : StorageProviderData {       private string databaseInstance;       private string getStoredProcedure;       private string setStoredProcedure;       public DatabaseStorageProviderData() : this(string.Empty)       {       }       public DatabaseStorageProviderData(string name) :           this(name, string.Empty, string.Empty, string.Empty)       {       }       public DatabaseStorageProviderData(                string name,                string dbInstanceString,                string getStoredProcedure,                string setStoredProcedure) :                base(name,                   typeof(DatabaseStorageProvider).AssemblyQualifiedName)       {            this.databaseInstance = dbInstanceString;            this.getStoredProcedure = getStoredProcedure;            this.setStoredProcedure = setStoredProcedure       }        [XmlAttribute("databaseInstance")]        public string DatabaseInstance        {            get { return databaseInstance; }            set { databaseInstance = value;}        }        [XmlAttribute("getStoredProcedure")]       public string GetStoredProcedure       {            get { return this.getStoredProcedure; }            set { this.getStoredProcedure = value; }       }        [XmlAttribute("setStoredProcedure")]       public string SetStoredProcedure       {            get { return this.setStoredProcedure; }            set { this.setStoredProcedure = value; }       }        [XmlIgnore]       public override string TypeName       {            get            {                 return                 typeof(DatabaseStorageProvider).AssemblyQualifiedName;            }            set            {            }       } } [Visual Basic] <XmlRoot("storageProvider", _           Namespace:=ConfigurationSettings.ConfigurationNamespace)> _ Public Class DatabaseStorageProviderData : Inherits StorageProviderData      Dim databaseInstance As String      Dim getStoredProcedure As String      Dim setStoredProcedure As String      Public Sub New()           Me.New(String.Empty)      End Sub      Public Sub New(ByVal name As String)           Me.New(name, String.Empty, String.Empty, String.Empty)      End Sub      Public DatabaseStorageProviderData(_                String name, _                String dbInstanceString, _                String getStoredProcedure, _                String setStoredProcedure)           MyBase.New(name, _                GetType(DatabaseStorageProvider).AssemblyQualifiedName)                Me.databaseInstance     = dbInstanceString                Me.getStoredProcedure     = getStoredProcedure                Me.setStoredProcedure     = setStoredProcedure      End Sub      <XmlAttribute("databaseInstance")> _      Public Property DatabaseInstance() As String           Get                Return databaseInstance           End Get           Set                databaseInstance = Value           End Set      End Property      <XmlAttribute("getStoredProcedure")> _      Public Property GetStoredProcedure() As String           Get                Return Me.getStoredProcedure           End Get           Set                Me.getStoredProcedure = Value           End Set      End Property      <XmlAttribute("setStoredProcedure")> _      Public Property SetStoredProcedure() As String           Get                Return Me.setStoredProcedure           End Get           Set                Me.setStoredProcedure = Value           End Set      End Property      <XmlIgnore> _      Public Overrides Property TypeName() As String           Get                Return _                GetType(DatabaseStorageProvider).AssemblyQualifiedName           End Get           Set           End Set      End Property End Class

Step 3: Create the Classes That Are Specific to This StorageProvider

This is the least straightforward part of creating a StorageProvider. The first task that needs to be completed for enabling a StorageProvider to detect changes in the underlying data store is to create ConfigurationChanged-EventArgs and ConfigurationChangingEventArgs for it. These classes are used as the arguments that get passed to the Configuration Application Block when a change occurs. They need to contain data for the ConfigurationChanged event to make it easy for event handlers to understand where a change may have occurred.

The classes are specific to the data described in the data transfer object for that StorageProvider. So, instead of passing in a filename like that used for XmlFileStorageProvider, the ConfigurationChangedEventArgs and ConfigurationChangingEventArgs for the DatabaseStorageProvider need the name of the DatabaseInstance and two stored procedures. It would be ideal to derive these from abstract base classes and simply supply the new values needed. However, in Enterprise Library, the ConfigurationChangedEventArgs is not an abstract base class.

Figure 1.5 shows the design for the ConfigurationChangedEventArgs in Enterprise Library. Because it assumes a file-based configuration is used, the base class, ConfigurationChangedEventArgs, contains member variables and properties relevant to a configuration file. This variable and property are vestigial for the SqlStorageProvider's and RegistryStorageProvider's ConfigurationChangedEventArgs. When these StorageProviders were created (which occurred after the initial release of Enterprise Library), their ConfigurationChangedEventArgs were derived from the base ConfigurationChangedEventArgs class and simply ignored the variable and property pertaining to the file. There's not really any harm done in doing this; passing in an empty string for the file name to the constructor will not cause any issues if the data store is not actually a file.

Figure 1.5. Enterprise Library's ConfigurationChangedEventArgs Design


This may seem like blasphemy to object-oriented "purists," and some architects are not comfortable with vestigial properties and ignoring variables. For the benefit of these architects, I have included a modified Configuration Application Block on the book's Web site. The modifications include the ConfigurationChangedEventArgs class, an abstract base class, and a new ConfigurationFileChangedEventArgs class that derives from it (and is a sibling to ConfigurationSqlChangedEventArgs and ConfigurationRegistryChangedEventArgs). This code is intended solely for those who must have a pure approach; other than that there is very little benefit to using it. In fact, I believe it to be a best practice to not modify the code supplied with Enterprise Library, but rather to wrap and extend it as needed. This makes it much easier to maintain and migrate your work as the Enterprise Library team releases patches and updates. Therefore, for the remainder of this example I have taken the "nonpurist" approach with respect to the ConfigurationChangedEventArgs.

Once it is understood that the file parameter will be ignored, the actual code for the EventArgs classes can easily be created. Listing 1.4 shows the code for the ConfigurationDatabaseChangedEventArgs class. The ConfigurationDatabaseChangingEventArgs is the exact same code except that it derives from ConfigurationChangingEventArgs instead of ConfigurationChangedEventArgs.

Listing 1.4. ConfigurationDatabaseChangedEventArgs Class

 [C#] [Serializable] public class ConfigurationDatabaseChangedEventArgs : ConfigurationChangedEventArgs {       private readonly string dbInstance;       private readonly string getConfig;       private readonly string setConfig;       public ConfigurationDatabaseChangedEventArgs(            string dbInstance,            string getConfig,            string setConfig,            string sectionName) : base(String.Empty,sectionName)        {            this.dbInstance = dbInstance;            this.getConfig = getConfig;            this.setConfig = setConfig;       }       public string DatabaseInstance       {            get { return this.dbInstance; }       }       public string GetConfigStoredProc       {            get { return this.getConfig; }       }       public string SetConfigStoredProc       {            get { return this.setConfig; }       } } [Visual Basic] <Serializable> _ Public Class ConfigurationDatabaseChangedEventArgs : _                      Inherits ConfigurationChangedEventArgs      Private ReadOnly dbInstance As String      Private ReadOnly getConfig As String      Private ReadOnly setConfig As String      Public Sub New(_                ByVal dbInstance As String, _                ByVal getConfig As String, _                ByVal setConfig As String, _                ByVal sectionName As String)                          MyBase.New(String.Empty,sectionName)            Me.dbInstance = dbInstance            Me.getConfig = getConfig            Me.setConfig = setConfig      End Sub      Public ReadOnly Property DatabaseInstance() As String           Get                Return Me.dbInstance           End Get      End Property      Public ReadOnly Property GetConfigStoredProc() As String           Get                Return Me.getConfig           End Get      End Property      Public ReadOnly Property SetConfigStoredProc() As String           Get                Return Me.setConfig           End Get      End Property End Class

Step 4: Create the ConfigurationChangeDatabaseWatcher

Now the ConfigurationChangeDatabaseWatcher can be created. This is the class that will raise the ConfigurationChanged event when the configuration data for a DatabaseStorageProvider has been modified. I know that Microsoft SQL Server 2005 and the .NET 2.0 Framework make it much easier to have a database call back into application code upon a change in the database; however, for now, I'll use what is available in the .NET 1.1 Framework to poll the configuration table and compare the lastmoddate value with the last one kept by the DatabaseStorageProvider. Incidentally, when the SQL script for this StorageProvider was run, an update trigger was created on the database table, so that modifications made to the table outside of an application that uses the DatabaseStorageProvider will still be detected.

Fortunately, Enterprise Library has taken the difficulty out of creating a ConfigurationChangeWatcher. An abstract base class, aptly named ConfigurationChangeWatcher, does all the "heavy lifting" that is needed. The ConfigurationChangeWatcher class spawns a background thread that will poll the derived class' GetCurrentLastWriteTime method to determine if a change has occurred. It compares this DateTime with a DateTime variable that it keeps to represent the last known update. If the value for the GetCurrentLastWriteTime is greater than the value for the local variable, the local variable is set to the GetCurrentLastWriteTime value and the ConfigurationChanged event is raised.

The ConfigurationChanged event expects the Configuration-ChangedEventArgs specific to that data store; for the DatabaseStorageProvider, this is the ConfigurationDatabaseChangedEventArgs created as the first task for this step. The base class retrieves this via the abstract BuildEventData method. Other than a few other helper methods that are used for diagnostic purposes, this is all that needs to be done to create a derived ConfigurationChangeWatcher.

Thus, the major activity that needs to be completed to create a new ConfigurationChangeWatcher is implementing the GetCurrentLastWriteTime method that is specific to the configuration data store. The value that is returned from this method begins the change notification process. The ConfigurationChangeDatabaseWatcher's GetCurrentLastWriteTime method is shown in Listing 1.5. The full source code for this class is on the book's Web site.

Listing 1.5. ConfigurationChangeDatabaseWatcher's GetCurrentLastWriteTime

 [C#] protected override DateTime GetCurrentLastWriteTime() {      DateTime currentLastWriteTime = DateTime.MinValue;      Database db;      try      {          db = DatabaseFactory.CreateDatabase(this.dbInstance);      }      // When the Config Console is used to modify this information,      // the database factory has to be used to dynamically build the      // dbInstance      catch (ConfigurationException)      {           ConfigurationContext ctx =                 runtimeConfigurationView.ConfigurationContext;           try           {                 DatabaseProviderFactory factory =                                new DatabaseProviderFactory(ctx);                 db = factory.CreateDatabase(dbInstance);           }           catch (Exception e)           {                throw new                     ConfigurationException(                     SR.ExceptionConfigurationDbInvalidSection(                          SectionName),e);           }      }      catch (Exception e)      {           throw new                ConfigurationException(                SR.ExceptionConfigurationDbInvalidSection(                     SectionName),e);      }      IDataReader dataReader =                db.ExecuteReader(this.getConfig, this.SectionName);      if( dataReader.Read() )           currentLastWriteTime = dataReader.IsDBNull( 1 ) ?           DateTime.MinValue : dataReader.GetDateTime( 1 );      dataReader.Close();      return currentLastWriteTime; } [Visual Basic] Protected Overrides Function GetCurrentLastWriteTime() As DateTime      Dim currentLastWriteTime As DateTime = DateTime.MinValue      Dim db As Database      Try           db = DatabaseFactory.CreateDatabase(Me.dbInstance)      ' When the Config Console is used to modify this information,      ' the database factory has to be used to dynamically build the      ' dbInstance      Catch e1 As ConfigurationException           Dim ctx As ConfigurationContext = _                runtimeConfigurationView.ConfigurationContext           Try                Dim factory As DatabaseProviderFactory = _                     New DatabaseProviderFactory(ctx)                db = factory.CreateDatabase(dbInstance)           Catch e As Exception                Throw New _                     ConfigurationException( _                           SR.ExceptionConfigurationDbInvalidSection( _                           SectionName),e)           End Try      Catch e As Exception           Throw New _                ConfigurationException( _                SR.ExceptionConfigurationDbInvalidSection( _                SectionName),e)      End Try      Dim dataReader As IDataReader = _           db.ExecuteReader(Me.getConfig, Me.SectionName)      If dataReader.Read() Then           currentLastWriteTime = IIf(dataReader.IsDBNull(1), _           DateTime.MinValue, dataReader.GetDateTime(1))      End If      dataReader.Close()      Return currentLastWriteTime End Function

The GetCurrentLastWriteTime method shown in Listing 1.5 is specific to the needs for the DatabaseStorageProvider. It creates a Database object via the Data Access Application Block and uses this object to get a DataReader that contains the last modified time, lastmoddate, for a configuration section. (The Data Access Application Block is covered in much more detail in Chapter 3.) Notice that there is no code that is specific to Microsoft SQL server in Listing 1.5. The information for connecting to the database and retrieving and providing data to it are maintained by the Data Access Application Block.

If the lastmoddate is greater than the last date and time that the base ConfigurationChangeWatcher class has recorded, then that class determines that a change has occurred and the ConfigurationChanged event is raised.

Otherwise, the thread sleeps for a predefined period before it calls the Data Access Application Block again.

Step 5: Create the DatabaseStorageProvider

The DatabaseStorageProvider is very similar to the SqlStorageProvider. Upon saving configuration data, the data is serialized and then saved in a database. When being retrieved, the data is fetched from the database and the configuration data is deserialized. The main differences are in the private LoadData and SaveData methods. Instead of specifically calling Microsoft SQL Server to retrieve and update information in a database, the Data Access Application Block is used.

Thus, the code for retrieving and updating the configuration data in the database is very similar to the code that exists for the GetCurrentLastWriteTime method in the ConfigurationChangeDatabaseWatcher. However, instead of just retrieving and updating the lastmoddate, the data for the configuration section is retrieved. Listing 1.6 shows the LoadData method. The full source code is on the book's Web site.

Listing 1.6. DatabaseStorageProvider's LoadData Method

 [C#] private XmlDocument LoadData() {      string xmlData;      Database db;      try      {           db = DatabaseFactory.CreateDatabase(DatabaseInstance);      }      // When the Config Console is used to modify this information,      // the database factory has to be used dynamically to build the      // dbInstance      catch (ConfigurationException configEx)      {           ConfigurationContext ctx =                 runtimeConfigurationView.ConfigurationContext;           try           {                 DatabaseProviderFactory factory =                      new DatabaseProviderFactory(ctx);                 db = factory.CreateDatabase(DatabaseInstance);           }           catch (Exception e)           {                throw new                     ConfigurationException(                           SR.ExceptionConfigurationDbInvalidSection                           (CurrentSectionName), e);           }      }      catch (Exception e)      {           throw new                ConfigurationException(                      SR.ExceptionConfigurationDbInvalidSection                      (CurrentSectionName), e);      }      lock(this)  {           try           {                IDataReader dataReader =                     db.ExecuteReader(                          GetStoredProcedure,                          CurrentSectionName);                if( !dataReader.Read() )                     return null;                else                     xmlData = dataReader.IsDBNull(0) ?                                    null : dataReader.GetString(0);                dataReader.Close();           }           catch (Exception e)           {                throw new                           ConfigurationException(                                 SR.ExceptionConfigurationDbInvalidSection                                 (CurrentSectionName), e);           }      } //End lock      if (xmlData == null || xmlData.Trim().Equals(String.Empty))      {           throw new                   ConfigurationException(                         SR.ExceptionConfigurationDbInvalidSection                         (CurrentSectionName));      }      return DeserializeDocumentData(xmlData); } [Visual Basic] Private Function LoadData() As XmlDocument      Dim xmlData As String      Dim db As Database      Try           db = DatabaseFactory.CreateDatabase(DatabaseInstance)           ' When the Config Console is used to modify this information,           ' the database factory has to be used dynamically to build the           ' dbInstance      Catch configEx As ConfigurationException           Dim ctx As ConfigurationContext = _                runtimeConfigurationView.ConfigurationContext           Try                Dim factory As DatabaseProviderFactory = _                     New DatabaseProviderFactory(ctx)                db = factory.CreateDatabase(DatabaseInstance)           Catch e As Exception                Throw New _                     ConfigurationException _                           (SR.ExceptionConfigurationDbInvalidSection _                           (CurrentSectionName), e)           End Try      Catch e As Exception           Throw New ConfigurationException _                      (SR.ExceptionConfigurationDbInvalidSection _                      (CurrentSectionName), e)      End Try      SyncLock Me           Try                Dim dataReader As IDataReader = _                          db.ExecuteReader( _                          GetStoredProcedure, _                          CurrentSectionName)                If (Not dataReader.Read()) Then                     Return Nothing                Else                     xmlData = IIf(dataReader.IsDBNull(0), _                                    Nothing, _                                    dataReader.GetString(0))                End If                dataReader.Close()           Catch e As Exception                Throw New ConfigurationException _                      (SR.ExceptionConfigurationDbInvalidSection _                      (CurrentSectionName), e)           End Try      End SyncLock      If xmlData Is Nothing OrElse xmlData.Trim().Equals(String.Empty) _      Then           Throw New ConfigurationException _                      (SR.ExceptionConfigurationDbInvalidSection _                      (CurrentSectionName))      End If      Return DeserializeDocumentData(xmlData) End Function

Step 6 (Post-Creation): Changing the Metaconfiguration

Configuring an application to use the DatabaseStorageProvider is a matter of configuring the values in the metaconfiguration for a specific configuration section. I have not yet detailed how you can use the Enterprise Library Configuration Tool to make this a very simple process, so for now you still need to configure this "by hand." The code in bold in Listing 1.7 shows how I have modified the metaconfiguration for the sample application that was described at the beginning of this chapter to use the DatabaseStorageProviderData instead of the XmlFileStorageProviderData.

Listing 1.7. Application Domain Configuration File Modified to Use the DatabaseStorageProvider

<?xml version="1.0" encoding="utf-8"?> <configuration>   <configSections>     <section name="enterpriselibrary.configurationSettings"           type="System.Configuration.IgnoreSectionHandler, System,           Version=1.0.5000.0, Culture=neutral,           PublicKeyToken=b77a5c561934e089" />   </configSections>   <enterpriselibrary.configurationSettings           xmlns:xsd="http://www.w3.org/2001/XMLSchema"           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           applicationName="Tabbed App"           xmlns="http://www.microsoft.com/practices/enterpriselibrary/                          08-31-2004/configuration">   <configurationSections>     <configurationSection            xsi:type="ReadOnlyConfigurationSectionData"            name="dataConfiguration" encrypt="false">            <storageProvider xsi:type="XmlFileStorageProviderData"                   name="XML File Storage Provider"               path="dataConfiguration.config" />               <dataTransformer                    xsi:type="XmlSerializerTransformerData"                    name="Xml Serializer Transformer">                    <includeTypes />               </dataTransformer>     </configurationSection>    <configurationSection name="TabConfig" encrypt="false">       <storageProvider xsi:type="DatabaseStorageProviderData"       name="Database Storage Provider"       databaseInstance="EntLibExtensions"       getStoredProcedure="EntLib_GetConfig"       setStoredProcedure="EntLib_SetConfig" />          <dataTransformer xsi:type="XmlSerializerTransformerData"                name="Xml Serializer Transformer">               <includeTypes />          </dataTransformer>       </configurationSection>     </configurationSections>     <keyAlgorithmStorageProvider xsi:nil="true" />     <includeTypes>     <includeType name="DatabaseStorageProviderData"          type="EnterpriseLibrary.Extensions.Configuration.          DatabaseStorageProviderData, EnterpriseLibrary.Extensions,          Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />        </includeTypes>     </enterpriselibrary.configurationSettings> </configuration>

Note that the includeType element also needs to be added to the metaconfiguration. Because the DatabaseStorageProvider is not compiled as part of the Configuration Application Block, the Configuration Application Block cannot automatically recognize how to create an instance of this provider. The Configuration Application Block needs to be told which assembly to use to create this provider. The includeType element provides the Configuration Application Block with this information.

After this configuration section has been modified to store its information in a database instead of an XML file, the results can be viewed in the centralized database. Figure 1.6 shows an example of these results.

Figure 1.6. Result of Using the DatabaseStorageProvider


The ITransformer Interface and the transformerProvider Class

The Enterprise Library version of the Configuration Application Block not only allows an application to be agnostic as to the data store in which its configuration data is held, but it also allows an application to be indifferent as to the structure of the data in the data store. Transformers, new in this version of the Configuration Application Block, translate between the format of the in-memory representation of the data and the format that is used by a StorageProvider. Earlier in this chapter, I demonstrated serializing an object that contains configuration data into an XML representation. The XmlSerializerTransformer that is included with Enterprise Library was used to transform the object into XML.

Applications are not required to configure a Transformer. If there is no Transformer, configuration settings objects are returned to the application in the same form as provided by the StorageProvider. If an XmlSerializerTransformer isn't used to deserialize XML, a consuming application has to deal with XML representation of the configuration object instead of the actual object. For this reason, it is often useful for an application to be configured to use a Transformer. This allows both the application and the StorageProvider to deal only with the representation of the data that each cares about (e.g., XML for the XMLFileStorageProvider) and not have to understand anything about creating classes from that representation.

The ITransformer interface has three methods: CurrentSectionName, Serialize, and Deserialize. Like the StorageProvider class, the Configuration Application Block supplies an abstract transformerProvider class that is intended to make it easy for developers to create Transformers. To create a Transformer, you only need to derive from the TransformerProvider class and implement the Deserialize and Serialize methods. Table 1.2 describes the purpose for each of these methods.

Table 1.2. Methods Needed by a Derived Transformer to Support the ITransformer Interface

Method

Description

Deserialize

Deserializes the configuration data coming from storage.

Serialize

Serializes the configuration data coming from the calling assembly and maps it into something that the StorageProvider can understand.


Enterprise Library Transformers

Enterprise Library originally shipped just one Transformer: the XmlSerializerTransformer. Figure 1.7 illustrates how this Transformer derives from the abstract transformerProvider base class and implements the ITransformer interface. This design allows other Transformers to be created and used in place of the XmlSerializerTransformer. After the initial release of Enterprise Library, the MvpSerializerTransformer was shipped with the Enterprise Library Extensions project as an alternate to using the XmlSerializerTransformer under special circumstances.

Figure 1.7. Enterprise Library Design for Transformers


XmlSerializerTransformer.

The Enterprise Library XmlSerializerTransformer can feed an XmlNode returned from a StorageProvider to the .NET XmlSerializer class so that it can be transformed into an object graph that represents configuration data. The XmlSerializerTransformer provides a generic way to serialize and deserialize configuration data without being bound to a specific object type.

The Serialize method wraps the results obtained from serializing the configuration data object with an element named xmlSerializationSection and an attribute named type. The value for the type attribute is the fully qualified name of the configuration data's object type. This allows the Deserialize method to work in reverse by first locating the node for the xmlSerializationSection. It then determines the type of class that needs to be deserialized from the attribute named type, and uses the XmlSerializer's Deserialize method to create a class from this type.

Because a generic XmlSerializer is used, the Transformer must inspect a specific node (the serializationNode) in the XML to determine the type of object that needs to be created. Furthermore, the first time the XmlSerializer object is used it must generate a class to read instances of the received type, compile it, and then load it. Unfortunately, this can result in very high startup costs and may prove to be unacceptable for some scenarios (e.g., smart client solutions).

MvpSerializerTransformer.

The MvpSerializerTransformer is offered as an alternative Transformer that can be used to reduce the startup costs that can be incurred when using the XmlSerializer. It works just like the XmlSerializerTransformer except that it uses precompiled serializers rather than the generic XmlSerializer, so there is no need to inspect the XML to determine the type of class that needs to be serialized and deserialized.

A tool is available on Daniel Cazzulino's blog that lets you create a precompiled XmlSerializer for any serializable class. [3] The custom serializer is automatically generated at design-time and the tool integrates nicely with Visual Studio. To get a design-time version of a custom XmlSerializer for a particular type, the Mvp.Xml.XGen tool must be associated and run for the configuration data class. This produces another class that can be used to perform serialization of the configuration data class. Figure 1.8 illustrates setting the Custom Tool property for the TabConfig class shown earlier in this chapter to Mvp.Xml.XGen to create the TabConfigSerializer class.

[3] See http://weblogs.asp.net/cazzu/archive/2004/10/21/XGenToolRelease.aspx.

Figure 1.8. Using Mvp.Xml.XGen to Create a Serializer


Once this class has been created, compiled, and deployed, it can be used by the MvpSerializerTransformer to serialize and deserialize a specific configuration data class; in the example used throughout this chapter, that is the TabConfig class. To use the MvpSerializerTransformer instead of the XmlSerializerTransformer, the configured Transformer must be replaced in the metaconfiguration and an include type must be added so the Configuration Application Block knows to load the proper assembly and type. Listing 1.8 highlights the changes that must be made to the metaconfiguration for the sample application to use the MvpSerializerTransformer instead of the XmlSerializerTransformer. The lines in bold highlight the changes that need to occur (some formatting has been done to enhance readability).

Listing 1.8. Metaconfiguration Changes to use the MvpSerializerTransformer

... <configurationSection name="TabConfig" encrypt="false">       <storageProvider xsi:type="DatabaseStorageProviderData"             name="Database Storage Provider"             databaseInstance="EntLibExtensions"             getStoredProcedure="EntLib_GetConfig"             setStoredProcedure="EntLib_SetConfig" />      <dataTransformer xsi:type="MvpSerializerTransformerData"            name="Mvp Serializer Transformer"            serializationType="TabConfiguration.TabConfigSerializer,            TabConfiguration, Version=1.0.2131.41118, Culture=neutral,            PublicKeyToken=null" />       </configurationSection>       ...       <includeTypes>            <includeType name="DatabaseStorageProviderData"                 type="EnterpriseLibrary.Extensions.Configuration.                DatabaseStorageProviderData, EnterpriseLibrary.Extensions,                 Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />          <includeType name="MvpSerializerTransformerData"                type="EnterpriseLibrary.Extensions.Configuration.                MvpSerializerTransformerData, EnterpriseLibrary.Extensions,                Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </includeTypes> ...

Creating a Custom Transformer

Most enterprises will probably never need a custom Transformer. However, for the rare case where the XmlSerializerTransformer or MvpSerializerTransformer is not good enough, I will take you through the steps to create one.

Creating a custom Transformer is not as arduous an exercise as creating a custom StorageProvider. There is no need to watch for configuration changes; a custom Transformer can be created by just deriving a new provider class from the abstract TRansformerProvider base class and overriding the Serialize and Deserialize methods.

Listing 1.9 shows a Transformer that doesn't use the XmlSerializer. This Transformer, the BinaryTransformer, serializes a configuration data class to its binary representation and deserializes it back again. This is not a bad option if you know that the schema for the configuration data is stable and the configuration data does not need to be modified in the data store which houses it. There are, however, a few items that should be recognized when using this Transformer.

  • The configuration data class must be Serializable in the remoting sense of the word. That is, the SerializableAttribute attribute should be applied to indicate that instances of the type can be serialized.[4]

    [4] See http://msdn.microsoft.com/library/default.asp?url=/library/enus/cpref/html/frlrfstserializableattributeclasstopic.asp for more information

  • The StorageProvider that is configured to save and load the data for this Transformer must know how to deal with binary data. The DatabaseStorageProvider shown earlier in this chapter has special methods for saving and retrieving binary data.

  • The configuration data will not be as easy to modify as it would be with the two Transformers provided by Enterprise Library. Because the configuration data will be stored as binary and not XML, the configuration data will have to be deserialized into an object graph and modified through an application instead of natively through an editor like Notepad or Visual Studio.

  • If the schema for the configuration data changes, it will be more difficult to modify the existing configuration data to match this schema.

Listing 1.9. Custom BinaryTransformer

[C#] public class BinaryTransformer : TransformerProvider {      public override object Serialize(object value)      {           ArgumentValidation.CheckForNullReference(value, "value");           byte[] buffer = null;           using (MemoryStream ms = new MemoryStream())           {                BinaryFormatter formatter = new BinaryFormatter();                formatter.Serialize(ms, value);                buffer = new byte[ms.Length];                ms.Position = 0;                ms.Read(buffer, 0, (int)ms.Length);                ms.Flush();           }           return buffer;      }      public override object Deserialize(object section)      {           object returnConfigObject = null;           BinaryFormatter formatter = new BinaryFormatter();           using (MemoryStream ms = new MemoryStream((byte[])section))           {                returnConfigObject = formatter.Deserialize(ms);           }           return returnConfigObject;      }      public override void Initialize                 (ConfigurationView configurationView)      {      } } [Visual Basic] Public Class BinaryTransformer : Inherits TransformerProvider      Public Overrides Function Serialize _                 (ByVal value As Object) As Object           ArgumentValidation.CheckForNullReference(value, "value")           Dim buffer As Byte() = Nothing           Dim ms As MemoryStream = New MemoryStream()           Try                Dim formatter As BinaryFormatter = _                          New BinaryFormatter()                formatter.Serialize(ms, value)                buffer = New Byte(ms.Length - 1) {}                ms.Position = 0                ms.Read(buffer, 0, CInt(ms.Length))                ms.Flush()           Finally                If TypeOf ms Is IDisposable Then                     Dim disp As IDisposable = ms                     disp.Dispose()                End If           End Try           Return buffer      End Function      Public Overrides Function Deserialize                 (ByVal section As Object) As Object            Dim returnConfigObject As Object = Nothing            Dim formatter As BinaryFormatter = New BinaryFormatter()            Dim ms As MemoryStream = _                      New MemoryStream(CType(section, Byte()))            Try                 returnConfigObject = formatter.Deserialize(ms)            Finally                 If TypeOf ms Is IDisposable Then                      Dim disp As IDisposable = ms                     disp.Dispose()            End If            End Try            Return returnConfigObject       End Function       Public Overrides Sub Initialize _                  (ByVal configurationView As ConfigurationView)       End Sub End Class

The BinaryTransformer can be configured in an application as a custom Transformer. Special hooks exist in Enterprise Library to call out to custom-built Transformers without needing to know intricate details about the data that a Transformer might need. In the case of the BinaryTransformer, it is especially useful because no additional information needs to be configured. The MvpSerializerTransformer, on the other hand, needed to know the type and assembly name for the class that was created with the Mvp.Xml.Gen tool. The BinaryTransformer does not need any additional information. If it did, though, a property named Attributes exists that can be used to pass configuration data to a custom Transformer. Attributes is a Dictionary object. A custom Transformer can get the values for the configuration data it needs by accessing a particular item in the Attributes Dictionary by name.

However, since none of that needs to be set for the BinaryTransformer, configuring an application to use this Transformer just involves modifying the metaconfiguration to replace the current Transformer with this one. In Listing 1.10, I have replaced the configuration for the MvpSerializerTransformer with the BinaryTransformer.

Listing 1.10. Metaconfiguration Changes to Use the BinaryTransformer

... <configurationSection name="TabConfig" encrypt="false">      <storageProvider xsi:type="DatabaseStorageProviderData"            name="Database Storage Provider"            databaseInstance="EntLibExtensions"            getStoredProcedure="EntLib_GetConfig"            setStoredProcedure="EntLib_SetConfig" />      <dataTransformer xsi:type=" CustomTransformerData"            name="Binary Transformer Provider"            serializationType=           "EnterpriseLibrary.Extensions.BinaryTranformer,            EnterpriseLibrary.Extensions" /> </configurationSection> ...

Managing Configuration Information

An application is able to take advantage of StorageProviders and Transformers to decouple itself from configuration data because the Configuration Application Block manages the work to make this happen. The Configuration Application Block also caches the configuration data for an application while still allowing the application to respond to changes that may occur in the underlying configuration data. This section focuses on the classes that exist in this block that provide these capabilities. Figure 1.9 illustrates how the classes in the Configuration Application Block work together to accomplish this.

Figure 1.9. Design of Enterprise Library's Configuration Application Block


Configuration Sections

Configuration settings for a block are grouped together and referred to as a configuration section. The Configuration Application Block accesses the data in a configuration section through an application's metaconfiguration. As illustrated in the previous section, the metaconfiguration includes information about the configuration storage location for a configuration section and the type of Transformer and StorageProvider that is needed to read from and write to this storage location.

Listing 1.11 shows two configuration sections as part of the metaconfiguration for an application. The first configuration section holds information for the Data Access Application Block and is named dataConfiguration. The metaconfiguration shows that the Configuration Application Block will use an XmlFileStorageProvider for storing the configuration data in a file named dataConfiguration.config. Furthermore, it will use an XmlSerializerTransformer for transforming the data from this StorageProvider into the object that is needed by the application. The configuration data for the Data Access Application Block contains information that is needed to connect to the relational databases that this application will use. That configuration data is kept in the dataConfiguration.config file. This is indicated by the value for the XmlFileStorageProvider in the metaconfiguration.

The second configuration section contains metaconfiguration for housing the special configuration information that this application needs. In this example, that is the TabConfig configuration data highlighted earlier in this chapter. This configuration section is configured to use a DatabaseStorageProvider to keep the configuration data in a relational database and an XmlSerializerTransformer to transform the data into the object graph needed by the application.

Listing 1.11. Sample Configuration Sections

... <configurationSections>      <configurationSection xsi:type="ReadOnlyConfigurationSectionData"             name="dataConfiguration" encrypt="false">        <storageProvider xsi:type="XmlFileStorageProviderData"             name="XML File Storage Provider"             path="dataConfiguration.config" />        <dataTransformer xsi:type="XmlSerializerTransformerData"             name="Xml Serializer Transformer" />        </configurationSection>        <configurationSection name="TabConfig" encrypt="false">          <storageProvider xsi:type="DatabaseStorageProviderData"              name="Database Storage Provider"              databaseInstance="EntLibExtensions"              getStoredProcedure="EntLib_GetConfig"              setStoredProcedure="EntLib_SetConfig" />          <dataTransformer xsi:type="CustomTransformerData"                 name="Binary Transformer Provider"                 type="EnterpriseLibrary.Extensions.BinaryTranformer,                      EnterpriseLibrary.Extensions" />      </configurationSection> </configurationSections> ...

The internal ConfigurationSections class keeps a cache of the configuration sections and ConfigurationChangeWatchers used to detect changes in the underlying configuration data stores. This means that caching the configuration settings in application code does not provide any performance benefits. If an application still chooses to cache configuration settings, then it is that application's responsibility to ensure that any changes to the configuration data are synchronized so that no portion of the application uses stale data. For this reason, the Microsoft patterns & practices team recommends that an application just let the Configuration Application Block handle the cache of configuration settings.

The ConfigurationSettings Class

Every application block includes a class that allows the configuration data for that block to be retrieved. This class can usually be recognized by the application block's suffix: Settings. Aside from the fact that the configuration data for the Configuration Application Block is the metaconfiguration, the Configuration Application Block is no different. The metaconfiguration for an application can be retrieved via the ConfigurationSettings class.

Information about the configuration sections can be accessed to retrieve data about the StorageProvider and Transformer configured for a section. Additionally, information about the XmlIncludeTypes needed by the Configuration Application Block, the name of the application, and an encryption key that can be used to encrypt the configuration data in the configuration sections can also be obtained. Table 1.3 lists the properties of the ConfigurationSettings class.

Table 1.3. ConfigurationSettings Properties

Property

Description

ConfigurationSections

Returns a collection of the configuration sections that have been configured for an application.

ApplicationName

Returns the name of the application as it appears in the metaconfiguration.

this[string] / Me(string)

Indexer for retrieving or setting a specific configuration section. For example, ConfigurationSettings["dataConfiguration"] would retrieve the configuration section for the Data Access Application Block shown in Listing 1-11.

KeyAlgorithmPairStorage-ProviderData

Retrieves the KeyAlgorithmPair's StorageProvider information that can be used to encrypt the data in a configuration section.

XmlIncludeTypes

Retrieves the collection of all include types that the Configuration Application Block needs.


Often times, the data that is contained in a configuration section must be protected. For example, the configuration section for the Data Access Application Block contains configuration data used to create connection strings to backend databases. This kind of sensitive information must be protected from those who should not see it. The Configuration Application Block allows the data in a configuration section to be protected by a combination of an encryption key and symmetric algorithm. This information is represented in the Configuration Application Block as a KeyAlgorithmPair.

When encryption for a configuration section has been enabled, the StorageProvider that is responsible for reading and writing the configuration data for that section uses an instance of the ConfigurationProtector class that is part of the Configuration Application Block. The ConfigurationProtector class gets the KeyAlgorithmPair information from ConfigurationSettings and makes functions available to the StorageProviders for easy encrypting and decrypting of the configuration data.

By default, the keys are saved in plaintext files. Any user or application with access to the plaintext file can read the key and use it to decrypt the configuration sections. Therefore, additional protection for this file should be implemented. An excellent option is to protect the file with the Data Protection API (DPAPI). The Enterprise Library Configuration Tool provides a wizard that can be used to create a key/algorithm pair. This wizard also enables DPAPI protection for the master key file. DPAPI is part of the operating system and can protect files in situations where other security measures, such as access control lists (ACLs), cannot. (Chapter 2 shows how to accomplish this by using the wizard provided by the Enterprise Library Configuration Tool.) For more information about DPAPI, see Chapter 8 in this book and the book Writing Secure Code, Second Edition, by Michael Howard and David C. LeBlanc.

XmlIncludeTypes allow the Configuration Application Block to be extended with object types that are not compiled in one of the base application blocks that ship with Enterprise Library. Some of the previous examples showed how Enterprise Library's Configuration Application Block can be extended by creating new StorageProviders and Transformers. But how does the Configuration Application Block know which type and assembly to use to create an instance of this class? The answer is that it uses XmlIncludeTypes.

The XmlIncludeTypes that are defined in the metaconfiguration, and thus maintained by the ConfigurationSettings, indicate which new types of objects the Configuration Application Block needs to instantiate at runtime.

The ConfigurationBuilder Object

The ConfigurationBuilder object is an internal class to the Configuration Application Block and represents the engine that manages the StorageProviders and Transformers that read and write the configuration data. When a ConfigurationBuilder object is created, it reads the metaconfiguration data from the application domain file (i.e., app.config or web.config) and stores this information in its private ConfigurationSettings member variable.

The ConfigurationBuilder contains the methods and events needed to read, write, and detect when changes are made to configuration files. In addition to a ConfigurationSettings member variable, the ConfigurationBuilder also maintains a ConfigurationSections variable. As mentioned earlier, the ConfigurationSections class keeps a cache of the configuration sections. Subsequent attempts to read configuration data use the cached data and do not reread information from the configuration storage. The Configuration Application Block can clear this cache for any and all configuration sections via the ClearSectionCache method if the StorageProvider detects that configuration data has changed in storage. If the cache is cleared, the next request for configuration information will access the configuration settings from the defined storage location.

The ConfigurationContext Object

The ConfigurationBuilder is intended to serve as a private, internal engine that manages the reading, writing, and caching of configuration information. Applications must use the ConfigurationManager and ConfigurationContext objects for reading, writing, and caching configuration and metaconfiguration data. The ConfigurationContext is not the way most applications will access configuration data; the ConfigurationManager (which is covered next) is intended to be the more common means.

The ConfigurationContext is intended to be used under special circumstances where an application or process needs more control of the configuration data than what the ConfigurationManager provides. The ConfigurationContext simply wraps around a ConfigurationBuilder object to read and write configuration and metaconfiguration data. It is actually the ConfigurationBuilder object that uses a StorageProvider and Transformer to do the heavy lifting of reading, writing, and transforming configuration data.

Whereas the ConfigurationManager's GetConfiguration and WriteConfiguration methods only support reading and writing configuration and metaconfiguration data using the application domain configuration file, the ConfigurationManager's CreateContext method can be used to create a ConfigurationContext object from a file other than an application domain configuration file. For example, an assembly that requires its own configuration independent of the host application might need to use this to retrieve metaconfiguration from a file other than the application domain configuration file. The file has to follow the same schema as the application domain configuration file, but other than that there are no constraints.

The ConfigurationContext object represents a read-only set of configuration data indexed by configuration section name. As such, it can be used to pass read-only configuration information throughout an application. The Configuration Application Block uses the ConfigurationContext by passing it throughout various methods so that all blocks in an application can consume their respective configuration information. This allows the configuration block to read in all of the configuration information once and pass that information around to all of the objects that need to get populated. Listing 1.12 shows how to get the object held in the ConfigurationContext for the TabConfig section.

Listing 1.12. Using the ConfigurationContext to Obtain Configuration Settings for a Section

[C#] ConfigurationContext context = ConfigurationManager.GetCurrentContext(); string file = context.ConfigurationFile; object data = context.GetConfiguration("TabConfig"); [Visual Basic] Dim context As ConfigurationContext = ConfigurationManager.GetCurrentContext() Dim file As String = context.ConfigurationFile Dim data As Object = context.GetConfiguration("TabConfig")

In the previous examples, the ConfigurationFile property and the GetConfiguration method were used to obtain the desired objects. The ConfigurationContext class contains many more properties and methods that can be used to read, write, and validate configuration data. Table 1.4 lists the ConfigurationContext's public methods and properties.

Table 1.4. ConfigurationContext Methods and Properties

Method/Property

Description

ConfigurationFile

Gets the configuration file used by the current context.

ClearSectionCache

Removes all sections from the internal cache.

GetConfiguration

Returns configuration settings for a configuration section.

GetMetaConfiguration

This overloaded method gets the metaconfiguration for the configuration manager.

IsReadOnly

Gets a value indicating whether a section in configuration is read-only.

IsValidSection

Determines if a section name is valid in the current configuration file.

WriteConfiguration

Writes the configuration for a section to storage.

WriteMetaConfiguration

This overloaded method writes the metaconfiguration for the configuration manager to the configuration file.


It might also be necessary to leverage a ConfigurationContext if an application or process needs to dynamically access configuration data without direct access to the underlying configuration data store. Such a need exists in the DatabaseStorageProvider example described earlier in this chapter. Under normal runtime conditions, the DatabaseStorageProvider works without any problems. However, a complexity occurs when this StorageProvider is configured with an application like the Enterprise Library Configuration Tool. I'll dive deeper into this tool and the Configuration Design-Time tool in the next chapter. For now, you just need to understand that because this StorageProvider relies on the configuration information of another application block (i.e., the Data Access Application Block), situations can occur when it cannot get this configuration information through the normal means.

When this occurs, a ConfigurationException is thrown and the DatabaseStorageProvider reacts by trying to access the Data Access Application Block's configuration information via the ConfigurationContext. This is a perfectly acceptable way to create a Database object and everything else flows through perfectly fine.

The ConfigurationManager Object

The other exposed interface for reading and writing configuration data is the ConfigurationManager object. The ConfigurationManager is simply a façade over the current ConfigurationContext class and is provided to make it easy to read and write configuration data. It provides static methods to read and write configuration settings for a specified configuration section. The static methods of the ConfigurationManager class simply use the current ConfigurationContext object to do its work. Table 1.5 lists the methods that can be used to read and write configuration data.

Table 1.5. ConfigurationManager Public Methods and Events

Method/Event

Description

ClearSingletonSectionCache

This overloaded method removes a section from the internal cache.

CreateContext

Gets a new instance of the ConfigurationContext class with the specified ConfigurationDictionary.

GetConfiguration

Returns configuration settings for a user-defined configuration section.

GetCurrentContext

Gets the ConfigurationContext for the current ConfigurationManager.

WriteConfiguration

Writes the configuration for a section to storage.

ConfigurationChanged (Event)

Occurs after the configuration is changed.

ConfigurationChanging (Event)

Occurs before the configuration is changed.





Fenster Effective Use of Microsoft Enterprise Library(c) Building Blocks for Creating Enterprise Applications and Services 2006
Effective Use of Microsoft Enterprise Library: Building Blocks for Creating Enterprise Applications and Services
ISBN: 0321334213
EAN: 2147483647
Year: 2004
Pages: 103
Authors: Len Fenster

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net