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>
<sectionGroup name="dataSourceProviders">
<section name="firstProviderDataSources"
type="FirstCompany.ProviderDemo.DataSource.ConfigurationSection,
FirstCompany.ProviderDemo.FirstProvider"/>
<section name="secondProviderDataSources"
type="SecondCompany.ProviderDemo.DataSource.ConfigurationSection,
SecondCompany.ProviderDemo.SecondProvider"/>
</sectionGroup>
</configSections>
<dataSourceProviders>
<firstProviderDataSources type="FirstCompany.ProviderDemo.DataSource.DataSource,
FirstCompany.ProviderDemo.FirstProvider">
<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">
<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 sections firstProviderDataSources
and secondProviderDataSources
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 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. - 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 applicationconnectionString
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.
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
{
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
{
internal class DataSource :
MainCompany.ProviderDemo.DataSource.IDataSource, IDisposable
{
private ConfigurationElement _configurationElement;
#region IDataSource Members
public void Open(System.Configuration.ConfigurationElement configurationElement)
{
_configurationElement = configurationElement as ConfigurationElement;
}
public void Close()
{
}
public string ReadData()
{
return ("Hello from the first provider, ConnectionString is " +
_configurationElement.ConnectionString);
}
#endregion
#region IDisposable Members
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 = new System.Configuration.ConfigurationProperty(
"type",
typeof(string),
null,
System.Configuration.ConfigurationPropertyOptions.IsRequired
);
_dataSources = new System.Configuration.ConfigurationProperty(
"",
typeof(ConfigurationElementCollection),
null,
System.Configuration.ConfigurationPropertyOptions.IsRequired |
System.Configuration.ConfigurationPropertyOptions.IsDefaultCollection
);
_properties = new System.Configuration.ConfigurationPropertyCollection();
_properties.Add(_type);
_properties.Add(_dataSources);
}
#endregion
#region Properties
[System.Configuration.ConfigurationProperty("type", IsRequired = true)]
public string Type
{
get
{
return (string)base[_type];
}
}
public System.Configuration.ConfigurationElementCollection DataSources
{
get { return (System.Configuration.ConfigurationElementCollection)
base[_dataSources]; }
}
protected override
System.Configuration.ConfigurationPropertyCollection Properties
{
get
{
return _properties;
}
}
#endregion
}
public class ConfigurationElementCollection:
System.Configuration.ConfigurationElementCollection
{
#region Properties
public override
System.Configuration.ConfigurationElementCollectionType CollectionType
{
get
{
return System.Configuration.ConfigurationElementCollectionType.BasicMap;
}
}
protected override string ElementName
{
get
{
return "firstProviderDataSource";
}
}
#endregion
#region Overrides
protected override System.Configuration.ConfigurationElement CreateNewElement()
{
return new ConfigurationElement();
}
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 = new System.Configuration.ConfigurationProperty(
"name",
typeof(string),
null, System.Configuration.ConfigurationPropertyOptions.IsRequired
);
_connectionString = new System.Configuration.ConfigurationProperty(
"connectionString",
typeof(string),
null,
System.Configuration.ConfigurationPropertyOptions.IsRequired
);
_properties = new System.Configuration.ConfigurationPropertyCollection();
_properties.Add(_name);
_properties.Add(_connectionString);
}
#endregion
#region Properties
[System.Configuration.ConfigurationProperty
("connectionString", IsRequired = true)]
public string ConnectionString
{
get { return (string)base[_connectionString]; }
set { base[_connectionString] = value; }
}
[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)
{
string typeName = dataSourceSection.Type.Split(',')[0];
string assemblyName = dataSourceSection.Type.Split(',')[1];
IDataSource dataSource = Activator.CreateInstance(assemblyName,
typeName).Unwrap() as IDataSource;
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