Creating a Custom Provider

Microsoft Enterprise Library 5.0

DropDown image DropDownHover image Collapse image Expand image CollapseAll image ExpandAll image Copy image CopyHover image

The following steps describe the process for creating a custom provider:

Preparing Your Project

To create a custom provider or extension, you must reference the following assemblies in your project, and optionally add using or Imports statements to your code for the namespaces they expose:

  • System.Configuration
  • Microsoft.Practices.EnterpriseLibrary.Common.dll
  • Microsoft.Practices.EnterpriseLibrary.Configuration.DesignTime.dll

You must also reference the Enterprise Library assembly that contains the interface or base class and the configuration element classes for the application block you wish to extend. For example, the custom Exception Handler in the sample project references the assembly Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll.

Implementing the Interface or Extending the Base Class

To create a custom provider, you implement the appropriate interface or inherit the appropriate base class, as listed in the tables in the topic Enterprise Library Extension Points. The configuration tool will only recognize and accept custom providers and extensions that follow this rule.

The interface or base class defines the methods and properties that your custom provider must implement. For example, if you decide to create a new Exception Handler for the Exception Handling Application Block, you must implement the interface IExceptionHandler. This interface defines one method that your handler must implement:

C# Copy Code
public interface IExceptionHandler
{
  Exception HandleException(Exception exception, Guid handlingInstanceId);
}
Visual Basic Copy Code
Public Interface IExceptionHandler
  Function HandleException(ByVal exception As Exception, _
                           ByVal handlingInstanceId As Guid) As Exception
End Interface

Specifying the Configuration Element Type

For the configuration system to recognize and use a custom provider or extension, you must add the ConfigurationElementType attribute to your class to specify the type of the configuration element your provider will use. The configuration element class is responsible for storing the configuration information for your application, creating the appropriate provider instance, and setting its properties.

If you decide to implement full integration, you must create a custom configuration element type and specify this as the parameter of a ConfigurationElementType attribute applied to your provider class. We describe this process in the section Adding Full Design-time Integration later in this topic.

If you decide not to implement full configuration tool integration, you simply specify the type listed in the Configuration Element (basic integration) column of the earlier tables of extension points. For example, if you are implementing a new exception handler for the Exception Handling Application Block, the custom configuration element you should use (defined in the tables in Enterprise Library Extension Points) is CustomHandlerData. Therefore, the class declaration for such a handler will look like this:

C# Copy Code
[ConfigurationElementType(typeof(CustomHandlerData))]
public class MyCustomExceptionHandler : IExceptionHandler
{ 
  ... 
}
Visual Basic Copy Code
<ConfigurationElementType(GetType(CustomHandlerData))> _ 
Public Class MyCustomExceptionHandler
  Implements IExceptionHandler
  ...
End Class

Some extension points do not have a configuration element that you can use. These types of providers or extensions cannot be configured through the configuration tools. You would typically instantiate them directly, and set any properties they might expose, in your application code.

Implementing a Suitable Constructor

The constructor for your custom provider or extension must be compatible with the configuration mode you choose:

  • If you choose basic integration, you must implement a constructor that accepts an instance of the NameValueCollection class (from the System.Collections.Specialized namespace). The configuration system will create a NameValueCollection instance containing all of the attribute names and values from the configuration element for your custom provider. See Creating a Constructor for Basic Integration.
  • If you choose full integration, you must create a constructor that is compatible with the code in your custom configuration element type class, which will create the instance of your provider or extension. See Adding Full Design-time Integration.

Creating a Constructor for Basic Integration

As an example of a constructor for a provider that uses basic integration, the custom Exception Handler in the example project for this guide is defined in the configuration file using the following element:

XML Copy Code
<add name="Simple File Log Handler" 
     type="SimpleFileLogHandler, SimpleFileLogHandler, Version=1.0.0.0"
     logFilePath="C:\Temp\BasicIntegration.txt" 
     exceptionMessage="An error occurred and was logged."
     wrapExceptionType="System.Exception, mscorlib, Version=2.0.0.0" 
/>

The constructor within the basic integration version of the example provider will receive a NameValueCollection instance containing the names and values of the name, type, logFilePath, exceptionMessage, and wrapExceptionType attributes. The constructor must extract the values it requires, validate them (to ensure that the user specified all the ones required, and that the values are within the appropriate ranges), and then save them for use within the provider when it is executed.

The following code shows an example constructor that uses the configuration file element listed above. This is taken from the example exception handler you can download for this guide. It validates and saves the values of the logFilePath, exceptionMessage, and wrapExceptionType attributes passed to it in class-level variables for use in the handler when it is executed.

C# Copy Code
public SimpleFileLogHandler(NameValueCollection configValues)
{   
  // Extract values from Name/Value collection and validate
  string logFilePath = configValues[configLogFilePath];
  string exceptionMessage = configValues[configExceptionMessage];
  string wrapException = configValues[configWrapException];
  if (null == logFilePath || null == wrapException 
      || logFilePath == String.Empty || wrapException == String.Empty)
  {
    throw new Exception("Invalid values in SimpleFileLogHandler configuration.");
  }
  else
  {
    // Save configuration values in local variables for use as required
    ...
  }
}
Visual Basic Copy Code
Public Sub New(ByVal configValues As NameValueCollection)
  ' Extract values from Name/Value collection and validate
  Dim logFilePath As String = configValues(configLogFilePath)
  Dim exceptionMessage As String = configValues(configExceptionMessage)
  Dim wrapException As String = configValues(configWrapException)
  If logFilePath Is Nothing OrElse wrapException Is Nothing _
      OrElse logFilePath = String.Empty OrElse wrapException = String.Empty Then
    Throw New Exception("Invalid values in SimpleFileLogHandler configuration.")
  Else
    ' Save configuration values in local variables for use as required
    ...
  End If
End Sub

After you have created your provider, you can place a copy of the assembly in the same folder as the configuration tool. This means that, when you select the Add Custom [provider type] menu item, the configuration tool will automatically display your custom provider type; as shown here.


However, if you do not copy the assembly to the configuration tool folder, you can still select it in the type selector dialog by clicking the Add from File button. Alternatively, if you strong name your provider and register it in the global assembly cache, you can select it by clicking the Add from GAC button.

Adding Full Design-time Integration

The configuration element class you specify in the ConfigurationElementType added to your provider class determines how your provider integrates with the configuration tools, and how it is instantiated at run time. When you use basic integration, as described in the previous sections of this guide, you take advantage of a class provided with Enterprise Library that does all the work of collecting configuration values, registering the provider, and passing the configuration values to it.

To achieve full integration in the configuration tools, you must create your own custom configuration element class that accomplishes these tasks. The class must:

  • Inherit the appropriate configuration base class, which manages integration with the configuration tools and the run time configuration system.
  • Implement a default constructor that calls the constructor of the configuration base class, specifying the type of your custom provider.
  • Expose public properties that the configuration tools will display, and apply attributes to these properties that specify the type of control or editor to use and the way values are displayed.
  • Override the GetRegistrations method of the base class to specify the type registration actions required to instantiate the provider.

The configuration base class you extend depends on the type of provider you are creating. The tables in the topic Enterprise Library Extension Points list the available types in the Configuration Element Base Class (design time integration) column. For example, the custom exception handler configuration class we discuss here extends the class ExceptionHandlerData.

Creating a Configuration Element Constructor for Full Integration

The configuration element class for your provider must implement a default constructor that takes no parameters. This is the constructor that the configuration system will use when it instantiates your provider. You can implement other constructors if you wish, though these are not used by the configuration system or the configuration tools. The following listing shows the two constructors in the example exception handler provided for this guide.

C# Copy Code
public class SimpleFileLogHandlerData : ExceptionHandlerData
{
  public SimpleFileLogHandlerData()
    : base(typeof(SimpleFileLogHandler))
  { }

  public SimpleFileLogHandlerData(string name, string logFilePath, 
                   string exceptionMessage, string wrapExceptionTypeName)
    : base(name, typeof(SimpleFileLogHandler))
  {
    LogFilePath = logFilePath;
    ExceptionMessage = exceptionMessage;
    WrapExceptionTypeName = wrapExceptionTypeName;
  }
  ...
}
Visual Basic Copy Code
Public Class SimpleFileLogHandlerData
  Inherits ExceptionHandlerData

  Public Sub New()
    MyBase.New(GetType(SimpleFileLogHandler))
  End Sub

  Public Sub New(ByVal name As String, ByVal _logFilePath As String, _
         ByVal _exceptionMessage As String, ByVal _wrapExceptionTypeName As String)
    MyBase.New(name, GetType(SimpleFileLogHandler))
    LogFilePath = _logFilePath
    ExceptionMessage = _exceptionMessage
    WrapExceptionTypeName = _wrapExceptionTypeName
  End Sub
  ...
End Class

Note:
If your provider does not require any configuration values passed to if from the configuration system, you must still implement a constructor that takes no parameters but calls the constructor of the base type. If you fail to do so, the compiler will create a default constructor and the provider will fail to correctly save its configuration.

Configuring Properties with Metadata Attributes

Your custom configuration element class must expose public properties for any values that you wish to be configurable in the configuration tools. For example, the sample custom exception handler configuration element exposes the LogFilePath, ExceptionMessage, WrapExceptionType, WrapExceptionTypeName properties.

To specify the appearance of your provider configuration within the configuration tools, you use a series of metadata attributes added to your configuration element class and its members. The following table lists the metadata attributes you can use.

Attribute

Description

BaseType

Specifies the base type of a property, the type to configure for the property, and how the type selector dialog displays types for this property.

Browsable

Specifies if the property is visible in the configuration tools. The default if omitted is true.

Category

ResourceCategory

Provides a mechanism for grouping related properties. The configuration tool groups all properties by category, and then sorts the properties within each category alphabetically. If the value is stored in a resources file instead of being supplied as a string value, use the ResourceCategory attribute instead of the Category attribute.

ConfigurationProperty

Specifies the name of the attribute stored in the configuration file for this property. Can also optionally specify the default value, whether a value is required, if it is the key for the element, and if it is the default collection.

Description

ResourceDescription

Specify the pop-up help text tooltip for this property. If the value is stored in a resources file instead of being supplied as a string value, use the ResourceDescription attribute instead of the Description attribute.

DesignTimeReadOnly

Specifies if the property value is read-only when displayed in the configuration tools. When true, the value cannot be modified by the user in the configuration tools. However, it can be modified by other components interacting with the property programmatically. The default if omitted is false. See also the ReadOnly attribute.

DisplayName

ResourceDisplayName

Specify the name of the property displayed in the configuration tools. If omitted, the property name is displayed. If the value is stored in a resources file instead of being supplied as a string value, use the ResourceDisplayName attribute instead of the DisplayName attribute.

Editor

Specifies the type of editor to use in the configuration tools to allow the user to edit the property value. The default edit control if this attribute is omitted is a text box. The types of editor you can use include CollectionEditor, ConnectionStringEditor, DatePickerEditor, FilteredFilePath, FrameworkElement, MultilineText, RegexTypeEditor, TemplateEditor, TypeSelector, and UITypeEditor.

EnvironmentalOverrides

Specifies if the property will be configurable in multiple environments when property overrides are in use in the configuration tools. The default if omitted is true.

NameProperty

Specifies the property of the provider that returns a value that is to be used as the name of the provider. You can optionally use the NamePropertyDisplayFormat named parameter as a template for the value; for example, NamePropertyDisplayFormat ="Field: {0}". The NameProperty attribute is optional. The provider will use the name of the configuration element if a value is not specified.

ReadOnly

Specifies if the value of the property is read-only. When true, the value cannot be modified by the user in the configuration tools, or by any other component interacting with the property programmatically. The default if omitted is false. See also the DesignTimeReadOnly attribute.

TypeConverter

Specifies the type of a TypeConverter class used to convert the property to and from a string when it is stored in the configuration file and passed to the property of the provider.

ViewModel

Used to replace the View Model implementation for the property with a custom version. Custom View Model implementations are outside the scope of this guide.


The example exception handler for this guide uses a range of these attributes in its configuration element class. For example, the class and each property carry the ResourceDescription and ResourceDisplayName attributes to specify the location of the localizable description and name strings in the project resources file. You can also see the attributes that specify the configuration attribute names, the type of UI editor to use, and the category:

C# Copy Code
[ResourceDescription(typeof(Resources), "SimpleFileLogDataDescription")]
[ResourceDisplayName(typeof(Resources), "SimpleFileLogDataDisplayName")]
public class SimpleFileLogHandlerData : ExceptionHandlerData
{
  ...
  [ConfigurationProperty(logFilePathProperty, IsRequired = true, 
                         DefaultValue = @"c:\temp\test.txt")]
  [ResourceDescription(typeof(Resources), "SimpleFileLogDataLogFilePathDescription")]
  [ResourceDisplayName(typeof(Resources), "SimpleFileLogDataLogFilePathDisplayName")]
  [Editor(CommonDesignTime.EditorTypes.FilteredFilePath,
          CommonDesignTime.EditorTypes.UITypeEditor)]
  public string LogFilePath
  { ... }

  [ConfigurationProperty(exceptionMessageProperty, IsRequired = false)]
  [Editor(CommonDesignTime.EditorTypes.MultilineText, 
          CommonDesignTime.EditorTypes.FrameworkElement)]
  public string ExceptionMessage
  { ... }

  [ConfigurationProperty(ExceptionMessageResourceTypeNameProperty)]
  [ResourceCategory(typeof(ResourceCategoryAttribute), "CategoryLocalization")]
  [Editor(CommonDesignTime.EditorTypes.TypeSelector, 
          CommonDesignTime.EditorTypes.UITypeEditor)]
  [BaseType(typeof(Object), TypeSelectorIncludes.None)]
  public string ExceptionMessageResourceType
  { ...}
  ...
}
Visual Basic Copy Code
<ResourceDescription(GetType(Resources), "SimpleFileLogDataDescription")> _
<ResourceDisplayName(GetType(Resources), "SimpleFileLogDataDisplayName")> _
Public Class SimpleFileLogHandlerData
  Inherits ExceptionHandlerData
  ...
  <ConfigurationProperty(logFilePathProperty, IsRequired:=True, _
                         DefaultValue:="c:\temp\test.txt")> _
  <ResourceDescription(GetType(Resources), _
                       "SimpleFileLogDataLogFilePathDescription")> _
  <ResourceDisplayName(GetType(Resources), _
                       "SimpleFileLogDataLogFilePathDisplayName")> _
  <Editor(CommonDesignTime.EditorTypes.FilteredFilePath, _
          CommonDesignTime.EditorTypes.UITypeEditor)> _
  Public Property LogFilePath() As String
    ... 
  End Property

  <ConfigurationProperty(exceptionMessageProperty, IsRequired:=False)> _
  <ResourceDescription(GetType(Resources), _
                       "SimpleFileLogDataExceptionMessageDescription")> _
  <ResourceDisplayName(GetType(Resources), _
                       "SimpleFileLogDataExceptionMessageDisplayName")> _
  <Editor(CommonDesignTime.EditorTypes.MultilineText, _
          CommonDesignTime.EditorTypes.FrameworkElement)> _
  Public Property ExceptionMessage() As String
    ....
  End Property

  <ConfigurationProperty(ExceptionMessageResourceTypeNameProperty)> _
  <ResourceDescription(GetType(Resources), _
                   "SimpleFileLogDataExceptionMessageResourceTypeDescription")> _
  <ResourceDisplayName(GetType(Resources), _
                   "SimpleFileLogDataExceptionMessageResourceTypeDisplayName")> _
  <ResourceCategory(GetType(ResourceCategoryAttribute), "CategoryLocalization")> _
  <Editor(CommonDesignTime.EditorTypes.TypeSelector, _
          CommonDesignTime.EditorTypes.UITypeEditor)> _
  <BaseType(GetType([Object]), TypeSelectorIncludes.None)> _
  Public Property ExceptionMessageResourceType() As String
    ...
  End Property

  ...
End Class
Note:
If you are building a custom provider in Visual Basic and you are using a resources file to provide values for the name and description (or other attributes) as shown in the code above, you must perform an extra task in this release of Enterprise Library. To allow the resources to be correctly resolved by the configuration tool, you must remove the custom tool namespace for the resources file:
- In Visual Studio Solution Explorer, click the Show All Files icon at the top of Solution Explorer.
- Select your resources file (by default this is Resources.resx).
- Press F4 to display the Properties window for the resources file.
- Remove the value for the Custom Tool Namespace property so that it is an empty string.


If you do not apply any attributes to a public property, it does not appear in the configuration tools. For example, the custom exception handler provider contains a public property named WrapExceptionType that is of type Type. However, to configure the type, the provider exposes the WrapExceptionTypeName property, which is of type String, and uses attributes to allow the user to configure this as a fully qualified type name. The provider then converts the type name to set its WrapExceptionType property to the appropriate Type instance. You may need to follow this approach in your customer providers and extensions to allow users to configure complex properties.

Overriding the GetRegistrations Method

Your custom configuration element must override the GetRegistrations method of the base class it extends to create the appropriate type registrations for your custom provider or extension. Your GetRegistrations method must return a collection of TypeRegistration instances; although, in the majority of cases, you will only require one type registration for your provider or extension class.

Each type registration defines a lambda expression that the configuration system will execute to register your provider with the dependency injection container that Enterprise Library uses to instantiate configured instances. The expression includes a call to the constructor of your custom provider type, together with code to set the values of the Name and Lifetime properties of the registration.

C# Copy Code
public override IEnumerable<TypeRegistration> GetRegistrations(string namePrefix)
{
  var exceptionMessageResolver = new ResourceStringResolver(
      ExceptionMessageResourceType, ExceptionMessageResourceName, ExceptionMessage);
  yield return
    new TypeRegistration<IExceptionHandler>(
      () => new SimpleFileLogHandler(
                LogFilePath, exceptionMessageResolver, WrapExceptionType))
      {
          Name = BuildName(namePrefix),
          Lifetime = TypeRegistrationLifetime.Transient
      };
}
Visual Basic Copy Code
Public Overloads Overrides Function GetRegistrations(ByVal namePrefix As String) _
                                    As IEnumerable(Of TypeRegistration)
  Dim exceptionMessageResolver = New ResourceStringResolver( _
      ExceptionMessageResourceType, ExceptionMessageResourceName, ExceptionMessage)
  Return New TypeRegistration() { _
    New TypeRegistration(Of IExceptionHandler)( _
      Function() New SimpleFileLogHandler(LogFilePath, exceptionMessageResolver, WrapExceptionType) _
    ) With {.Name = BuildName(namePrefix), .Lifetime = TypeRegistrationLifetime.Transient} _
  }
End Function

Typically, you will register your provider with a transient lifetime so that a new instance is created each time an application resolves the provider from the container. However, you can specify a container-controlled lifetime using TypeRegistrationLifetime.Singleton if this is appropriate for your provider.

Creating a Provider Constructor for Full Integration

After you create your custom configuration element class, you must specify this as the configuration element type for your custom provider or extension using the ConfigurationElementType attribute. Then you can implement a suitable constructor in your provider or extension class.

As you have just seen, the GetRegistrations method of your configuration element class specifies the constructor and its parameter values that the configuration system will use to instantiate your provider or extension. Therefore, you must implement a corresponding constructor in your provider or extension class. This constructor must accept as parameters all the configuration values you will expose through the configuration system.

The class that implements the example exception handler provider carries the ConfigurationElementType attribute specifying our custom configuration element class SimpleFileLogHandlerData, as you can see in the following listing. The listing also shows the constructor that the configuration element will register. This constructor forwards to a second constructor (not shown) that translates the values into the appropriate types and stores them in local variables for use in the provider at run time.

C# Copy Code
[ConfigurationElementType(typeof(SimpleFileLogHandlerData))]
public class SimpleFileLogHandler : IExceptionHandler
{ 
  public SimpleFileLogHandler(string logFilePath, string exceptionMessage, 
                            Type wrapExceptionType)
    : this(logFilePath, new ConstantStringResolver(exceptionMessage),
           wrapExceptionType)
  { }

  ...
} 
Visual Basic Copy Code
<ConfigurationElementType(GetType(SimpleFileLogHandlerData))> _
Public Class SimpleFileLogHandler
  Implements IExceptionHandler

  Public Sub New(ByVal logFilePath As String, ByVal exceptionMessage As String, _
                 ByVal wrapExceptionType As Type)
    Me.New(logFilePath, New ConstantStringResolver(exceptionMessage), _
           wrapExceptionType)
  End Sub

  ...
End Class

Installing Your Fully Integrated Provider

The final step is to compile your custom provider project so that the assembly contains the configuration element class and the custom provider or extension type, then copy the assembly into the configuration tool folder. Now you can start the configuration tool and open the appropriate section, then open the Add ... menu item in the section in which your custom provider resides. You will see your new provider in the list, as shown in this screenshot for the example exception handler provider described in this guide.

Summary of Steps

This section summarizes the steps for creating both a basic and a fully integrated provider for Enterprise Library, as described in detail in this guide:

  • Prepare Visual Studio by creating a new project and adding references to the required assemblies and namespaces.
  • Decide whether you will implement basic or full integration with the confirmation system.
  • Locate the appropriate interface or base class that you will implement or extend, and create your custom provider class to implement or extend this.
  • If you are creating a provider with basic integration:
    • Implement a constructor in your provider class that accepts the configuration values as a NameValueCollection instance, and parse these values into the properties of your provider.
    • Add the ConfigurationElementType attribute to the provider class and use it to specify the appropriate configuration element type that is included within Enterprise Library.
    • Compile your project, and optionally copy your provider assembly into the same folder as the configuration tools.
    • Load your provider into your application configuration by selecting the appropriate Add Custom [provider type] menu item in the configuration tools, and selecting the assembly and type of your provider.
    • Set the Key and Value for each name/value pair using the Attributes editor section for your provider in the configuration tools.
  • If you are creating a provider with full integration:
    • Implement a constructor in your provider that accepts all of the configuration values you require at run time.
    • Add the ConfigurationElementType attribute to the provider class and use it to specify the custom configuration element type class that you will create.
    • Create the custom configuration element type class by inheriting the appropriate configuration base class.
    • Implement a default constructor that calls the constructor of the configuration base class, specifying the type of your custom provider.
    • Expose public properties that the configuration tools will display, and apply attributes to these properties that specify the type of control or editor to use and how values are displayed.
    • Override the GetRegistrations method of the base class to specify the type registration actions required to instantiate the provider.
    • Compile your project, and copy your provider assembly into the same folder as the configuration tools.
    • Load it into your application configuration by selecting the Add [your provider name] menu item in the configuration tools.
    • Set the values for the properties using the types of UI controls and editors specified in your custom configuration element type class.

The following screenshot shows the standalone configuration console with the configuration file for the example exception handler solution loaded. You can see the two versions of the custom handler configured for two separate exception handling policies.