devxlogo

Adding Configuration Support for Custom Providers in Enterprise Library in ASP.NET 2.0

Adding Configuration Support for Custom Providers in Enterprise Library in ASP.NET 2.0

his is the last entry in a series of articles on Microsoft’s “patterns & practices” (p&p) Enterprise Library tools, and how you can use them in your ASP.NET applications. Enterprise Library provides a range of application blocks and features that make it easier to accomplish complex tasks such as accessing databases, caching and encrypting data, handling exceptions, and generating Event Log messages. It demonstrates and codifies best practices in design and implementation. Microsoft supplies Enterprise Library in source code form so you can learn from it?and adapt it to suit your own specific requirements.

A previous article in this series showed how easy it is to build a custom provider for an Enterprise Library application block. The article discussed building a custom backing store provider for the Caching Application Block, but the principles for creating providers that extend Enterprise Library are the same irrespective of the block that you need to extend.

However, custom providers are effectively second-class citizens within Enterprise Library because you can configure them only as an instance of the pre-defined custom provider type, and you must use a NameValueCollection as the Attributes property in the Configuration Console to pass values from the configuration into the provider at runtime. These restrictions are both unintuitive and error-prone, because users have no visual indication of the number of name/value pairs required, or the name and type of each one. In addition, the Configuration Console cannot enforce rules such as which values are required, which are optional, and what the valid data type is for each.

Configuration Console Design Support
The good news is that you can add classes and code to your provider that allows it to fully integrate with the Configuration Console, and become a first-class citizen within Enterprise Library. To achieve you must satisfy three basic conditions:

  • Your provider must expose a constructor that takes as parameters the values the user will configure in the console.
  • You must create one or more classes that store and expose the configuration data in the configuration file to your provider.
  • You must create one or more classes that represent the nodes in the console tree view that the console uses during the configuration process.
?
Figure 1. New Configuration Console Support: Here’s the Configuration Console after adding support for the Custom File Cache Storage provider.

Fulfilling these conditions requires some additional sub-steps?you must also:

  • Specify text strings for display in the console during the configuration process.
  • Create a class that ObjectBuilder uses to generate your custom provider configuration.
  • Modify the classes that add items to the menus in the configuration console and handle user interaction with these menus.

All this sounds like a daunting prospect and a complicated process, but in fact it is relatively simple as long as you take an ordered and careful approach to modifying the existing Enterprise Library classes. In this article, you’ll see how to add Configuration Console design support to the custom file caching provider created in the previous article. At the end of this process, it will integrate into the Configuration Console as shown in Figure 1, where you can see that the Cache Manager menu now contains a “Custom File Cache Storage” option.

Adding Configuration Console support makes the process much easier and more intuitive (and discoverable) for users when selecting a custom provider. They no longer need to locate the containing assembly and specify the type because the Configuration Console now already knows the provider type. In addition, the Configuration Console can display a list of the properties required for the provider, and you can cause it to display specific controls or dialogs to make setting the properties easier and less error-prone. Figure 2 shows how users can set the CacheFilePath property of the custom provider using a Windows Save File dialog.

?
Figure 2. Custom Dialog: The figure shows the Configuration Console using a Save File dialog to set the CacheFilePath property for the Custom File Cache Storage provider.

Adding Configuration Design Support
The process for adding design support involves five main tasks:

  1. Determine the provider configuration requirements and decide which properties/attributes the configuration file will contain.
  2. Modify your custom provider class. Change the constructor parameters in your custom provider class, and make any other modifications required, so that it accepts separate parameters for each configuration attribute rather than receiving them all as a single NameValueCollection. Also change the configuration attribute on your class to specify the configuration data class type that your class will use within the Configuration Console.
  3. Create the configuration data and configuration node classes that describe the custom provider (including the attributes it requires), an assembler class that manages the build process for the class in ObjectBuilder, and classes that describe the properties for the provider and the way that the console exposes them for editing.
  4. Edit the resources file in the Design section to add entries for the text and commands displayed by the console for this provider node
  5. Execute node and command registration by editing the block-specific class that inherits from NodeMapRegister so that it registers the new provider node with the console, and the block-specific class that inherits from CommandRegister so that it registers the strings in the resources file as commands the console will display when configuring the new provider.

The rest of the sections in this article explore each of these five tasks in more detail. Note that a simple provider such as the custom cache backing store described in this article requires only a single configuration data class and a single configuration node class. You need more only if your provider needs to expose configurable child nodes in the console and therefore requires parent/child configuration settings (such as multiple configurable paths for a file caching provider).

Determining the Provider Configuration Requirements
First you need to decide what configuration values you need to persist for your provider. These are the attributes that will appear in the element for the provider, excluding the type and name attributes that the configuration system automatically provides through the base classes you will extend. The values will appear as properties in the Configuration Console (from the configuration node classes you create), and be passed to your class constructor at runtime (through the configuration data classes you create).

The example cache backing store provider requires only one property?a String that is the full path to the folder where it will create the cache files?persisted via the path attribute in the configuration file. You’re not limited to strings; you can persist any needed value type, and the configuration system will convert it to the correct type at runtime. However, if you need to persist reference types (such as child node types and their values), you must create separate configuration node and configuration data classes for those. For some good examples, take a look at the way that the Caching Application Block and other application blocks provide classes like this in their Configuration and Design subfolders.

Modifying the Custom Provider Class
You need to make only simple modifications to the existing provider class to support the configuration. I’ve changed the class name to CustomFileBackingStore in the downloadable example code so you can install both the basic custom provider from the previous article and this design-configured provider without naming conflicts. The class that will provide the runtime configuration data is named CustomFileCacheStorageData. You will see details of this class later. The provider class requires a ConfigurationElementType attribute that specifies the configuration data class, which you add to the class definition:

   [ConfigurationElementType(typeof(CustomFileCacheStorageData))]   public class CustomFileBackingStore : BaseBackingStore   ...

The only other change is to the provider class constructor. Now it must accept parameters that are of the correct typed value. In the example, the only parameter required is the path to the cache file folder:

   public CustomFileBackingStore(String cacheFilePath)   {         // store the path and filename provided by the parameter      if (cacheFilePath != String.Empty)      {         filePath = cacheFilePath;      }      else      {         throw new Exception("Error in application configuration, '"            + filePathConfigurationName + "' attribute not found");      }   }

The rest of the code is the same as before. Note that the constructor still checks that there is a value for the path attribute in the configuration file (which appears as the cacheFilePath parameter to the constructor). The Configuration Console will force users to provide this attribute/property, but remember that users may edit the configuration file manually, outside of the console, so you should still check that any required values are present within your constructor.

Creating the Configuration Data and Configuration Node Classes
The provider relies on a configuration data class that exposes the configuration at runtime. Create a new class within the Configuration subfolder of the block you are extending, and add the required namespace references. For the example caching backing store provider, you need these references:

   using System.Configuration;   using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;   using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.      ObjectBuilder;   using Microsoft.Practices.EnterpriseLibrary.Caching.      BackingStoreImplementations;   using Microsoft.Practices.ObjectBuilder;   ...

The usual approach is to use this file to hold both the configuration data class and its associated assembler class. The configuration data class carries an Assembler attribute that specifies the assembler class that Object Builder uses to create the configuration data class instance.

Author’s Note: ObjectBuilder is a utility that underlies several patterns & practices tools, including the Composite UI Application Block (CAB), the Mobile Client Software Factory (MCSF), and Enterprise Library. ObjectBuilder provides a pipeline that allows code to create or retrieve instances of objects pre-configured to meet the needs of the code, through a series of strategies that take place in stages such as PreCreation, Creation, Initialization, and PostInitialization. An Assembler class you create in your own code uses ObjectBuilder to construct the final object and instantiate it through hard coded values for the object type to create, the constructor requirements of that class, and any extra work required to initialize the object before returning it. For more information on the ObjectBuilder and Assembler classes, see Brian Button’s “One Agile Coder” blog.

The next listing shows the configuration data class, which should inherit from a suitable base class such as (in this case) CacheStorageData. It also carries the Assembler attribute that specifies the class CustomFileBackingStoreAssembler. Inside the configuration data class, you must provide a default constructor that takes no parameters, and a constructor that takes the name parameter (the name of the custom provider) and any properties the provider requires. In this example, the only property is the path to the cache file folder:

   ...   namespace Microsoft.Practices.EnterpriseLibrary.Caching.Configuration   {      [Assembler(typeof(CustomFileBackingStoreAssembler))]      public class CustomFileCacheStorageData : CacheStorageData      {         private const string pathNameProperty = "path";            public CustomFileCacheStorageData()         {         }            public CustomFileCacheStorageData(string name, string pathName)            : base(name, typeof(CustomFileBackingStore))         {            this.CacheFilePath = pathName;         }         ...

Note that the preceding code saves the provider-specific parameter value in a local variable and exposes it as a public property. The configuration data class must expose the properties of the provider so that the configuration system and the provider can read and set them. For each property, add a ConfigurationProperty attribute that makes that property visible in the Configuration Console, specifies the name of the attribute in the configuration file, and indicates whether the property is optional or required, as shown below:

       ...       [ConfigurationProperty(pathNameProperty, IsRequired = true)]       public string CacheFilePath       {           get { return (string)base[pathNameProperty]; }           set { base[pathNameProperty] = value; }       }     }     ...

The example provider has only one property, named CacheFilePath. In the preceding code, the local variable pathNameProperty holds the attribute name (“path”), which is required. The base class property collection stores the property value, so the property accessor indexes into that collection to retrieve and set the value.Adding an Assembler Class
The assembler class, within the same file, must implement the IAssembler interface, which has a single method named Assemble. Your method implementation will receive a range of variables that the configuration system makes available to the constructor of the concrete implementation of the IAssembler interface within the core Enterprise Library configuration system. The method code casts this to the configuration data type CustomFileCacheStorageData, and then creates an instance of your provider class by calling its constructor and specifying the values for its properties using the values exposed by the configuration data class:

      ...      public class CustomFileBackingStoreAssembler          : IAssembler      {         public IBackingStore Assemble(IBuilderContext context,             CacheStorageData objectConfiguration,            IConfigurationSource configurationSource,             ConfigurationReflectionCache reflectionCache)         {            CustomFileCacheStorageData castedObjectConfiguration               = (CustomFileCacheStorageData)objectConfiguration;               IBackingStore createdObject = new CustomFileBackingStore(               castedObjectConfiguration.CacheFilePath);               return createdObject;         }      }      }
Author’s Note: Probably the easiest way to create your assembler class, depending on the block you are extending, is to copy the assembler class from one of the built-in providers that implements the same provider interface as your custom provider. For example, in this case, the assembler class is adapted from the built-in Isolated Storage provider’s configuration data class?CustomFileCacheStorageData.

Building the Configuration Node Class
The Configuration Console uses the configuration node class for your provider to create the node in the configuration tree view, and to present the provider properties to users for editing. Typically, you should create this class within the ConfigurationDesign subfolder of the block you are extending. However, the Enterprise Library Visual Studio solution that opens from your Start menu contains a separate project for the design features of each application block, and if you’re using that solution you should create your configuration node class within the appropriate project.

Next, add the required namespaces to your class. For the example provider, you need the following:

   using System.ComponentModel;   using System.Drawing.Design;   using Microsoft.Practices.EnterpriseLibrary.      Configuration.Design.Validation;   using Microsoft.Practices.EnterpriseLibrary.      Caching.Configuration.Design.Properties;   using System;   using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;   ...

Your configuration node class should inherit from an appropriate storage node base class, as shown in the next listing, depending on the block you are extending. For the example provider, the appropriate base class is CacheStorageNode.

The configuration node class must also expose constructors that allow the Configuration Console to create instances of the class with default property values for new configurations, and with pre-configured values if the user is opening an existing configuration that contains the custom provider. For the example provider, the default constructor calls the constructor of the base class specifying the default name for this node (extracted from the resources file), and an empty string for the CacheFilePath property:

   ...   namespace Microsoft.Practices.EnterpriseLibrary.      Caching.Configuration.Design   {      public class CustomFileCacheStorageNode : CacheStorageNode      {         private string pathName;         public CustomFileCacheStorageNode() : this(new             CustomFileCacheStorageData(Resources.DefaultFileCacheNodeName,            string.Empty))         {         }         ...

When a user opens an existing configuration containing this provider, the second constructor takes a reference to the configuration data class containing the existing configuration information, checks that the reference is not null, and sets the Name property of the underlying base class (CacheStorageNode) to the name specified in the configuration. Then it sets all the provider property values using the values specified in the existing configuration?in this case, the path.

         ...         public CustomFileCacheStorageNode(            CustomFileCacheStorageData fileCacheStorageData)         {            if (fileCacheStorageData == null)            {               throw new ArgumentNullException("CustomFileCacheStorageData");            }            Rename(fileCacheStorageData.Name);            this.pathName = fileCacheStorageData.CacheFilePath;         }         ...

The remainder of the configuration node class contains the property accessor for each property configurable in the console, and for the property that exposes the populated configuration data to the provider. You use attributes to specify how the Configuration Console will handle and expose each property that users can configure.

The configuration node class for the example provider has to expose only the CacheFilePath property (again, the Name and Type properties are exposed by the base class). The attributes for this class indicate that the property value is required, and that the Configuration Console should present a SaveFileEditor (a class within the core Enterprise Library configuration that displays a File Save dialog) instance to users when they edit this property. The FilteredFileNameEditor attribute specifies the resource name of a text string in the resources file that the underlying Save File dialog should use as the file type filter (“All files (*.*)|*.*” in this example). Finally, the SRCategory and SRDescription attributes specify string resources that the Configuration Console will display in the status bar and as help text when configuring this property:

         ...         [Required]         [Editor(typeof(SaveFileEditor), typeof(UITypeEditor))]         [FilteredFileNameEditor(typeof(Resources),              "FileCachePathFileDialogFilter")]         [SRCategory("CategoryGeneral", typeof(Resources))]         [SRDescription("TextAreaNameDescription", typeof(Resources))]         public string CacheFilePath         {            get { return pathName; }            set { pathName = System.IO.Path.GetDirectoryName(value); }         }   

The property accessor itself just retrieves and sets the value of the local class-level variable named pathName. However, one minor issue is that the SaveFileEditor class forces users to specify a file name, and does not allow selection of an empty folder. Therefore, the set clause must strip off the filename that the user selects or enters. You might consider cloning the existing SaveFileEditor class and modifying it to allow selection of a folder, or creating your own more suitable descendant of the UITypeEditor base class, if you require this or more specialized functionality in your custom provider(s).

Finally, you must override the public CacheStorageData property of the CacheStorageNode base class to expose the current instance of the CustomFileCacheStorageData class that contains the configuration information:

       public override CacheStorageData CacheStorageData       {         get { return new CustomFileCacheStorageData(Name, pathName); }       }     }   } 

Editing the Design Resources File
Several values used in the configuration node class discussed in the previous section were text strings extracted from the resource file for the application block, including the status bar and help text strings used in the Configuration Console and the Save File dialog filter. You’ll see more code that uses string resources in the following sections. Placing these language-specific strings in the project resources file makes future updates easier, and allows you to support multiple languages if required.

To add these text strings to the project resources file, open the file Resources.resx from within the Properties folder of the Caching.Configuration.Design project. Add the entries in Table 1 to the file using the resource editor that appears when you open the file:

Table 1. String Resources: The table shows the names and values of the string resources added to the project resources file and used in the menus, the status bar, as help text, and as a filter for the Save File dialog.
String Resource NameValue
FileCacheNameDescriptionGets or sets the path and name of the cache disk file.
FileCachePathFileDialogFilterAll files (*.*)|*.*
FileCacheUICommandLongTextAdd custom file cache storage
FileCacheUICommandTextCustom File Cache Storage

Executing Node and Command Registration
The final stage in enabling design support for your provider is to edit the two classes that register the nodes in the Configuration Console tree view, and add the commands to the main and shortcut menus. The Enterprise Library configuration system allows application blocks to override the Register method in the two classes that register tree view nodes (the NodeMapRegistrar class) and menu commands (the CommandRegistrar class).

The Design project for the Caching Application Block contains two classes that extend the NodeMapRegistrar and CommandRegistrar classes?CachingNodeMapRegistrar and CachingCommandRegistrar. You add your own registration code to the Register methods of these classes.

For the CachingNodeMapRegistrar class, you simply insert a call to the AddSingleNodeMap method, specifying the text to display in the configuration console for the item, and the types to use for the configuration node and the configuration data:

   ...   AddSingleNodeMap(Resources.FileCacheUICommandText,      typeof(CustomFileCacheStorageNode),      typeof(CustomFileCacheStorageData));   ...

For the CachingCommandRegistrar class, you create a routine that calls the AddSingleChildNodeCommand method, specifying the command text for the menus, the text to display in the status bar, the type of the provider configuration node class, the type of the base configuration node class, and the type of the base node class:

   private void AddFileCacheStorageCommand()   {      AddSingleChildNodeCommand(Resources.FileCacheUICommandText,         Resources.FileCacheUICommandLongText,              typeof(CustomFileCacheStorageNode),         typeof(CacheStorageNode), typeof(CacheManagerNode));   }

Then you modify the Register method to insert a call to your new routine, followed by a call to the AddDefaultCommands method that specifies the type of the provider configuration node class:

   ...   AddFileCacheStorageCommand();   AddDefaultCommands(typeof(CustomFileCacheStorageNode));   ...

Compiling and Deploying the Provider
You have now created all the classes you need, and completed the modifications to existing classes. The next step is to compile the projects and copy the assemblies into the correct folders. The easiest way to achieve this is to use the two utilities available from the Enterprise Library section of your Start menu?”Build Enterprise Library” and “Copy Assemblies to bin Folder.”

Configuring the Custom File Cache Provider
Open the Configuration Console from the Enterprise Library section of your Start menu and either create a new application, or open an existing Web.config or App.config file. If you start a new application, or the existing configuration does not already contain the Caching Application Block, right-click on the Application Configuration entry, select “New,” and click “Caching Application Block.”

Within the Cache Managers section, select the Cache Manager node (or the existing child node that is a renamed cache manager node), right-click, and select “New.” The shortcut menu shows the new provider type “Custom File Cache Storage,” which uses the custom provider with the design support described earlier in this article. As you hover the mouse over this item, the status bar shows the text “Add custom file cache storage” (see Figure 3). Alternatively, you can select “New” and then “Custom File Cache Storage” from the Action menu when the Cache Manager node is selected in the tree view.

?
Figure 3. Custom Configuration Support: The figure shows the Configuration Console when selecting the “Custom Cache File Storage” option that uses the example provider.
?
Figure 4. Using the Configuration Console: The figure shows the process of changing the name and setting the CacheFilePath property for the example provider.

When you select the new Custom File Cache Storage item in the tree view, the right-hand pane displays the two properties you can edit (see Figure 4). You can change the name of this Cache Manager, and you can set the value of the CacheFilePath property.

In Figure 4, notice that the editor for the FileCachePath property, although it is defined as a String value, displays a “browse” button with an ellipsis () when you select it. Clicking this opens a File Save dialog so you can navigate to the folder where you want to store the cache files. However, as discussed earlier, because of the nature of the dialog class, you must either enter a name for a file or select an existing file. If you select an existing file, clicking the “Save” button prompts you to over-write it. However, once the dialog closes, the property contains just the path because the property accessor code (as you saw in the earlier section) removes the filename. Obviously, this is not an ideal situation, but to implement a better path selection mechanism, you must write a custom UITypeEditor class.

Using the Custom File Cache Provider in ASP.NET
In use, the configurable custom cache backing store provider discussed in this article behaves exactly the same at runtime as the non-design-configured version you saw in the previous article, because the only change made to the provider in this article was the addition of design support for the Configuration Console. The code that performs the caching operations is identical.

Figure 5 shows the Web.config file for the ASP.NET example application open in the Enterprise Library Configuration Console. You can see both the Cache Manager that uses the Isolated Storage Backing Store provider, and the second Cache Manager that uses the Custom File Backing Store provider described in this article. The CacheFilePath property is set to a temporary folder on the local machine.

?
Figure 5. Configuration in ASP.NET: The figure shows the Caching Application Block configuration for the ASP.NET example application.
?
Figure 6. Adding a DataSet: When you add a DataSet to the cache using the custom caching provider, the application displays the number of items in the cache.
?
Figure 7. Retrieving the DataSet: When you retriev the DataSet from the cache using the custom caching provider, the application displays the items in the dataset.

When you select the Custom Disk File Cache option in the ASP.NET example application (see Figure 6), the code uses the CustomFileBackingStore provider to write a DataSet to the cache, and then displays the number of items in the cache.

When you click the button to retrieve the DataSet, you see it displayed in the page?just as in the examples in the previous articles in this series (see Figure 7).

This article is the last of a series on using Enterprise Library in your ASP.NET applications, showing how you can add design support to custom providers so that they integrate fully with Enterprise Library, becoming indistinguishable from the built-in providers. For more information, an excellent Enterprise Library reference is the Addison-Wesley book Effective Use of Microsoft Enterprise Library: Building Blocks for Creating Enterprise Applications and Services, which will help you understand the workings of each block, the Enterprise Library core and configuration systems, and the best approaches for using and extending the application blocks.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist