Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

.NET 2.0 Configuration and Provider Model

4.44/5 (7 votes)
27 Mar 2010GPL34 min read 1   191  
Use .NET 2.0 configuration features for building a pluggable provider framework for your application.

Introduction

The article shows how one can use .NET 2.0 configuration classes when building an application that supports pluggable provider model.

Imagine we are working for a company called, say, MainCompany Inc. and we are developing a complex application that does some data processing. What we want is an open architecture that would allow other developers to implement providers that:

  • can be easily plugged into the main application
  • can support arbitrary data sources

The following diagram gives the idea of what our architecture should look like:

Image 1

The main application developer has no a priori knowledge about the nature of the data sources that can be used, but may specify the way it uses the data. For the sake of simplicity, we will assume that the main application just needs to get some text data from every data source once in a while.

Configuration File

The following is a sample config file that defines two providers and three data sources:

XML
<configuration>
  <configSections>
    <!-- Define custom configuration section group here -->

    <sectionGroup name="dataSourceProviders">
      <!-- Define custom configuration sections -->
      <section name="firstProviderDataSources" 
            type="FirstCompany.ProviderDemo.DataSource.ConfigurationSection, 
            FirstCompany.ProviderDemo.FirstProvider"/>
      <section name="secondProviderDataSources" 
            type="SecondCompany.ProviderDemo.DataSource.ConfigurationSection, 
            SecondCompany.ProviderDemo.SecondProvider"/>

    </sectionGroup>
  </configSections>
  <!-- This is our custom configuration section group -->
  <dataSourceProviders>
    <!-- Custom configuration sections: each defines a set of data sources 
    managed by a single provider -->
    <firstProviderDataSources type="FirstCompany.ProviderDemo.DataSource.DataSource, 
        FirstCompany.ProviderDemo.FirstProvider">

      <!-- Data sources managed by first provider -->
      <firstProviderDataSource name="DataSourceA" 
        connectionString="server=box-a;database=SomeDatabase"/>
      <firstProviderDataSource name="DataSourceB" 
        connectionString="server=box-b;database=SomeOtherDatabase"/>

    </firstProviderDataSources>
    <secondProviderDataSources type="SecondCompany.ProviderDemo.DataSource.DataSource, 
            SecondCompany.ProviderDemo.SecondProvider">
      <!-- Data sources managed by second provider -->
      <secondProviderDataSource name="DataSourceC" sourceMachine="192.168.0.1"/>

    </secondProviderDataSources>
  </dataSourceProviders>
</configuration>

If you want to get familiar with config sections, config section groups and config elements, you may want to check out an excellent article by Jon Rista. Here is a brief overview of the config file.

  1. Define custom config section group called dataSourceProviders that contain custom config sections firstProviderDataSources and secondProviderDataSources handled by correspondent classes:
    • FirstCompany.ProviderDemo.DataSource.ConfigurationSection
    • SecondCompany.ProviderDemo.DataSource.ConfigurationSection
  2. Add firstProviderDataSources config section with two elements, each element says the application should create an instance of FirstCompany.ProviderDemo.DataSource.DataSource class and pass connectionString parameter to the newly created data source. Name property serves as a unique id for the data source and is used by the main application to identify data sources.
  3. Add secondProviderDataSources config section with one element. That element says the application should create an instance of SecondCompany.ProviderDemo.DataSource.DataSource class and pass sourceMachine parameter to the newly created data source. Name property serves as a unique id for the data source and is used by the main application to identify data sources.

The key things to understand are:

  • name property of the config element is a "well-known" property and can be used by the main application
  • connectionString and sourceMachine properties are provider-specific, the main application knows nothing about them.

Interfaces

Let's discuss the "Data Access Provider Model" module contents and how they are used. All interfaces that the main application should be familiar with are defined in this module and should be implemented by third party data source providers.

Interfaces.gif

DataSource Interface

This interface defines the way in which the main application gets data from providers. Let's keep it simple: Open, ReadData and Close methods are enough.

C#
using System.Configuration;
namespace MainCompany.ProviderDemo.DataSource
{
    /// Sample IDataSource interface that declares a couple of simple methods
    public interface IDataSource
    {
        void Open(ConfigurationElement configurationElement);
        void Close();
        string ReadData();
    }
}

The key is the ConfigurationElement object passed to the data source. It may contain any provider-specific settings that data source may need.

Configuration interfaces

IDataSourceConfigurationSection gives the main application access to the data sources managed by the provider and tells it which actual data source class it has to instantiate for every provider.

C#
namespace MainCompany.ProviderDemo.DataSource
{
    public interface IDataSourceConfigurationSection
    {
        string Type
        {
            get;
        }

        System.Configuration.ConfigurationElementCollection DataSources
        {
            get;
        }
    }
}

IDataSourceConfigurationElement interface exposes data source name to the main application.

C#
namespace MainCompany.ProviderDemo.DataSource
{
    public interface IDataSourceConfigurationElement
    {
        string Name
        {
            get;
        }
    }
}

Provider Implementation

Consider a simple data source provider developed by some third party called, say, FirstCompany.

IDataSource Implementation

The implementation is pretty straightforward. Please note that this implementation uses the FirstCompany.ProviderDemo.DataSource.ConfigurationElement object to access provider-specific settings (connectionString in this case).

C#
namespace FirstCompany.ProviderDemo.DataSource
{
    /// Sample implementation of IDataSource interface
    internal class DataSource : 
        MainCompany.ProviderDemo.DataSource.IDataSource, IDisposable
    {
        private ConfigurationElement _configurationElement;

        #region IDataSource Members

        /// Sample implementation, just caches configuration settings
        public void Open(System.Configuration.ConfigurationElement configurationElement)
        {
            _configurationElement = configurationElement as ConfigurationElement;
        }

        /// Sample implementation, does nothing
        public void Close()
        {
            // Close if needed
        }

        /// Sample implementation, returns provider description and 
        /// provider-specific property value
        public string ReadData()
        {
            return ("Hello from the first provider, ConnectionString is " + 
                    _configurationElement.ConnectionString);
        }

        #endregion

        #region IDisposable Members

        /// Sample implementation
        public void Dispose()
        {
            Close();
        }

        #endregion
    }
}

Using .NET 2.0 Configuration Framework

Now let's get all the config file plumbing done. The following couple of classes will do the minimal config section/element handling.

C#
namespace FirstCompany.ProviderDemo.DataSource
{
    public class ConfigurationSection : System.Configuration.ConfigurationSection, 
        MainCompany.ProviderDemo.DataSource.IDataSourceConfigurationSection
    {
        #region Fields

        private static System.Configuration.ConfigurationPropertyCollection _properties;
        private static System.Configuration.ConfigurationProperty _type;
        private static System.Configuration.ConfigurationProperty _dataSources;

        #endregion

        #region Constructors

        static ConfigurationSection()
        {
            // Type of the data source object, our framework will use it when
            // creating an instance of data source through Activator
            _type = new System.Configuration.ConfigurationProperty(
                "type",
                typeof(string),
                null,
                System.Configuration.ConfigurationPropertyOptions.IsRequired
            );

            // This is the default property of the section and it holds all data source
            // configuration elements that are managed by this provider
            _dataSources = new System.Configuration.ConfigurationProperty(
                "",
                typeof(ConfigurationElementCollection),
                null,
                System.Configuration.ConfigurationPropertyOptions.IsRequired | 
                    System.Configuration.ConfigurationPropertyOptions.IsDefaultCollection
                );

            // Add property definitions to the collection
            _properties = new System.Configuration.ConfigurationPropertyCollection();
            _properties.Add(_type);
            _properties.Add(_dataSources);
        }

        #endregion

        #region Properties

        /// This property implements IDataSourceConfigurationSection method 
        /// so the framework can read the "type" property value 
        /// and instantiate correspondent data source object
        [System.Configuration.ConfigurationProperty("type", IsRequired = true)]
        public string Type
        {
            get
            {
                return (string)base[_type];
            }
        }

        /// This property implements IDataSourceConfigurationSection method 
        /// so the framework can walk through all data sources managed by this provider
        public System.Configuration.ConfigurationElementCollection DataSources
        {
            get { return (System.Configuration.ConfigurationElementCollection)
                    base[_dataSources]; }
        }

        /// ConfigurationSection override, returns the list of all properties
        /// (including the default property - element collection)
        protected override 
               System.Configuration.ConfigurationPropertyCollection Properties
        {
            get
            {
                return _properties;
            }
        }

        #endregion
    }
    public class ConfigurationElementCollection: 
              System.Configuration.ConfigurationElementCollection
    {
        #region Properties

        /// ConfigurationElementCollection override, defines collection type
        public override 
             System.Configuration.ConfigurationElementCollectionType CollectionType
        {
            get
            {
                return System.Configuration.ConfigurationElementCollectionType.BasicMap;
            }
        }

        /// ConfigurationElementCollection override, defines XML element name
        protected override string ElementName
        {
            get
            {
                return "firstProviderDataSource";
            }
        }

        #endregion

        #region Overrides

        /// ConfigurationElementCollection override, 
        /// creates a new element of our custom type
        protected override System.Configuration.ConfigurationElement CreateNewElement()
        {
            return new ConfigurationElement();
        }

        /// ConfigurationElementCollection override, gives element key
        /// (it's the Name property in our implementation, we assume it is unique)
        protected override object GetElementKey
           (System.Configuration.ConfigurationElement element)
        {
            return (element as ConfigurationElement).Name;
        }

        #endregion
    }
    public class ConfigurationElement : System.Configuration.ConfigurationElement, 
        MainCompany.ProviderDemo.DataSource.IDataSourceConfigurationElement
    {
        #region Static Fields

        private static System.Configuration.ConfigurationPropertyCollection _properties;
        private static System.Configuration.ConfigurationProperty _name;
        private static System.Configuration.ConfigurationProperty _connectionString;

        #endregion

        #region Constructors

        static ConfigurationElement()
        {
            // Name of the data source, required for 
            // IDataSourceConfigurationElement support
            _name = new System.Configuration.ConfigurationProperty(
                "name",
                typeof(string),
                null, System.Configuration.ConfigurationPropertyOptions.IsRequired
                );

            // This is a purely provider-specific property
            _connectionString = new System.Configuration.ConfigurationProperty(
                "connectionString",
                typeof(string),
                null,
                System.Configuration.ConfigurationPropertyOptions.IsRequired
                );

            // Add property definitions to the collection
            _properties = new System.Configuration.ConfigurationPropertyCollection();
            _properties.Add(_name);
            _properties.Add(_connectionString);
        }

        #endregion

        #region Properties

        /// connectionString property
        [System.Configuration.ConfigurationProperty
              ("connectionString", IsRequired = true)]
        public string ConnectionString
        {
            get { return (string)base[_connectionString]; }
            set { base[_connectionString] = value; }
        }

        /// name property and IDataSourceConfigurationElement Name method implementation
        [System.Configuration.ConfigurationProperty("name", IsRequired = true)]
        public string Name
        {
            get { return (string)base[_name]; }
            set { base[_name] = value; }
        }

        #endregion
    }
}

I am not going to explain the details here, Jon has them all covered in his article. Just note that our config classes also implement IDataSourceConfigurationSection and IDataSourceConfigurationElement interfaces that are recognized by the main application.

Putting It All Together

Now let's develop an (extremely simple) application that utilizes our provider model. It walks through all data sources described in the config file, opens them, reads data and closes them.

C#
using System.Configuration;
using MainCompany.ProviderDemo.DataSource;
using System.Runtime.Remoting;

namespace MainCompany.ProviderDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Configuration configuration = ConfigurationManager.OpenExeConfiguration
                (ConfigurationUserLevel.None);
            ConfigurationSectionGroup sectionGroup = configuration.GetSectionGroup
                ("dataSourceProviders");
            foreach (IDataSourceConfigurationSection dataSourceSection in 
                sectionGroup.Sections)
            {
                foreach (IDataSourceConfigurationElement dataSourceElement in 
                dataSourceSection.DataSources)
                {
                    // Instantiate a datasource
                    string typeName = dataSourceSection.Type.Split(',')[0];
                    string assemblyName = dataSourceSection.Type.Split(',')[1];
                    IDataSource dataSource = Activator.CreateInstance(assemblyName, 
                        typeName).Unwrap() as IDataSource;

                    // Open data source and get some data from it
                    dataSource.Open(dataSourceElement as ConfigurationElement);
                    Console.WriteLine("Message from datasource " + 
                        dataSourceElement.Name +": " + dataSource.ReadData());
                    dataSource.Close();
                }
            }
        }
    }
}

Build the app and make sure that config file and provider assemblies are in the same folder as the app binary. Run the application. It should communicate with all providers specified in the config file and display messages from all data sources:

Message from datasource DataSourceA: Hello from the first provider, 
    ConnectionString is server=box-a;database=SomeDatabase

Message from datasource DataSourceB: Hello from the first provider, 
    ConnectionString is server=box-b;database=SomeOtherDatabase

Message from datasource DataSourceC: Hello from the second provider, 
    sourceMachine is 192.168.0.1

Next Steps

Next steps probably are:

  • Check out a commercial product called CSWorks that uses this technique
  • Make IDataSource interface look more realistic
  • Make provider framework hot-swappable: all config changes should take effect immediately after the user changes the config file, no application/service restart required
  • Implement an API that utilizes .NET 2.0 config classes and allows an application to modify config file in a convenient way
  • Enjoy the benefits of the open architecture

History

  • 21st May, 2008: Initial post
  • 26th March, 2010: Article updated

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)