.NET 2.0 Configuration and Provider Model






4.44/5 (7 votes)
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:

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:
<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.
- Define custom config section group called
dataSourceProviders
that contain custom config sectionsfirstProviderDataSources
andsecondProviderDataSources
handled by correspondent classes: FirstCompany.ProviderDemo.DataSource.ConfigurationSection
SecondCompany.ProviderDemo.DataSource.ConfigurationSection
- Add
firstProviderDataSources
config section with two elements, each element says the application should create an instance ofFirstCompany.ProviderDemo.DataSource.DataSource
class and passconnectionString 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. - Add
secondProviderDataSources
config section with one element. That element says the application should create an instance ofSecondCompany.ProviderDemo.DataSource.DataSource
class and passsourceMachine
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 applicationconnectionString
andsourceMachine
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.

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.
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.
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.
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).
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.
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.
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