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