How to Extend the Provider Model to Make It Easy to Use
Posted by Dan Rigsby on February 22nd, 2008
The Provider Model has been around for quite awhile now and its in use all over the .Net Framework and Enterprise Library since .Net 2.0. The provider model allows you to program against an intermediary, while the actual implementation can be defined in the app.config file. The best example of this is a data access layer. You might program a generic data access layer, but the actual implementation may be for Sql Server or Oracle and can be decided in the app.config file. I have used the provider model to abstract things such as a data access layer, a geocoding service, an authentication system, etc. Its nice to be able to just create a different implementation and swap it out in the app.config filer.
There is a decent amount of information out there (including a recent post by Joel Ross) and there is built in support for it in System.Configuration.Provider. However, while the tools in .Net make it possible to use the Provider Model, there is little guidance and some missing features in this namespace. This article is about how to extend the provider model to make it easier to use, and give a little insight into how it might be used. We are going to specifically look at the following areas:
- ProviderBase extensions:
- Exposing the configuration values through the provider so they can be used later in the code
- Using the provider type as the default name if no name is specified
- How to create a ProvidersSection that allows you to specify the default provider to use
- ProviderCollection extensions:
- Add Generics support
- Add the ability to get an Provider by index instead of just by name
- How to build a generic ProviderRepository
Note: At the bottom of this post I have zipped up all of the code and samples in this article. The sample included is a complete working example of the basic principles outlined here.
Definitions
Abstract Provider: an abstract class that services as a base for your providers. This usually contains all abstract methods and properties or overloaded methods and implementation details that can be shared by all providers. This usually derives from ProviderBase.
Provider: the actual implementation of an abstract base provider. For instance, this could be your actual SQL Server implementation of the abstract data provider.
ProvidersSection: the configuration section used by providers in the app.config file
ProviderCollection: a collection of providers
ProviderRepository: a repository that abstracts the need to manually try to determine which provider to use from the app.config file and wraps up tools needed to work with provider and the entire collection of providers.
ProviderBase Extensions
When working with the provider model each provider is named. This is important so that each provider in the list can be recognized from the others such as:
<DataService defaultProvider="SqlDataProvider"> <providers> <add name="SqlDataProvider" type="DataAccessLayer.SqlClient.SqlDataProvider, DataAccessLayer.SqlClient" /> <add name="OracleDataProvider" type="DataAccessLayer.OleDb.OracleDataProvider, DataAccessLayer.OleDb" /> </providers> </DataService>
As you can see, each provider has a name and a type. Here is my extended implementation of ProviderBase:
using System; using System.Collections.Specialized; using System.Configuration; using System.Configuration.Provider; namespace Rigsby.Configuration.Provider { /// <summary> /// Provides a base implementation for the extensible provider model. /// </summary> public class ProviderBase : System.Configuration.Provider.ProviderBase { #region Private Properties private NameValueCollection m_Configuration = new NameValueCollection(); private bool m_IsInitialized = false; private string m_Name = String.Empty; #endregion Private Properties #region Public Properties /// <summary> /// Gets the configuration. /// </summary> /// <value>The configuration.</value> public NameValueCollection Configuration { get { return m_Configuration; } } /// <summary> /// Gets or sets the friendly name used to refer to the provider during configuration. /// </summary> /// <value></value> /// <returns>The friendly name used to refer to the provider during configuration.</returns> public new string Name { get { return m_Name; } set { m_Name = value; } } /// <summary> /// Gets a value indicating whether this instance is initialized. /// </summary> /// <value> /// <c>true</c> if this instance is initialized; otherwise, <c>false</c>. /// </value> public bool IsInitialized { get { return m_IsInitialized; } } #endregion Public Properties #region Overrides /// <summary> /// Initializes the provider. /// </summary> public void Initialize() { Initialize(String.Empty); } /// <summary> /// Initializes the provider. /// </summary> /// <param name="name">The friendly name of the provider.</param> public void Initialize( string name) { Initialize(name, new NameValueCollection()); } /// <summary> /// Initializes the provider. /// </summary> /// <param name="name">The friendly name of the provider.</param> /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param> public override void Initialize( string name, NameValueCollection config) { if (config == null) { config = new NameValueCollection(); } // Get default name if empty or null if (String.IsNullOrEmpty(name)) { if (!String.IsNullOrEmpty(m_Name)) { name = m_Name; } else { name = this.GetType().Name; } } m_Name = name; // Store configuration values m_Configuration.Clear(); foreach (string key in config.Keys) { m_Configuration.Add(key, config[key]); } // Call the base class's Initialize method base.Initialize(name, config); m_IsInitialized = true; } #endregion Overrides } }
The default System.Configuration.Provider.ProviderBase will hide all of the configuration values after Initialize is called. I wanted to expose this collection so that you can add in custom configuration attributes easily and access them later in your code. The default provider base would also throw exceptions if there were any unknown attributes. I found this to be undesirable. If I wanted to add a new attribute that only one of my providers might need, I would want to be able to add it without it on the fly without having to extend my provider and make sure it understood the attribute. So now based on this new ProviderBase we can do things like this:
// Configuration file <DataService defaultProvider="SqlDataProvider"> <providers> <add name="SqlDataProvider" type="DataAccessLayer.SqlClient.SqlDataProvider, DataAccessLayer.SqlClient" useStoredProcedure="true" /> </providers> </DataService> // Code if (provider.Configuration["useStoredProcedure"]) { // handle stored procedures }
This may not seem like much, but it can be a real time saver if you use the Provider Model a lot and need inject custom attributes.
Creating a ProvidersSection
In our previous example, we showed the list of providers an app.config file and the root configuration section specified a "defaultProvider" attribute. This is an example of the custom ProvidersSection. It’s just a generic ConfigurationSection that allows for a "defaultProvider" attribute and a collection of ProviderSettings. You can extend this to add your own attributes, but for most purposes any custom attributes you will need will be on the provider settings themselves and not on the ProvidersSection.
using System; using System.Configuration; namespace Rigsby.Configuration.Provider { /// <summary> /// A configuration section for a collection of providers. /// </summary> public class ProvidersSection : ConfigurationSection { private const string m_DefaultProviderProperty = "defaultProvider"; /// <summary> /// Gets the providers. /// </summary> /// <value>The providers.</value> [ConfigurationProperty("providers")] public ProviderSettingsCollection Providers { get { return (ProviderSettingsCollection)base["providers"]; } } /// <summary> /// Gets or sets the default provider. /// </summary> /// <value>The default provider.</value> [ConfigurationProperty(m_DefaultProviderProperty, IsRequired=false)] public string DefaultProvider { get { return (string)base[m_DefaultProviderProperty]; } set { base[m_DefaultProviderProperty] = value; } } } }
ProviderCollection Extensions
The default ProviderCollection does not contain support for generics. This means you have to do a lot of extra casting and things aren’t strongly typed. It is also setup to only allow retrieval of providers by name. In some cases you need to be able to get the provider by index. Suppose you want to setup the provider model to try to use the first provider in the list, and if that doesn’t work, then use the next provider, and so on. If you could only get the providers by name, there is no way to easily get the next provider. The custom ProviderCollection shown below adds generics support and the ability to retrieve a provider by index.
using System; using System.Configuration; using System.Configuration.Provider; namespace Rigsby.Configuration.Provider { /// <summary> /// Represents a collection of provider objects that inherit from <see cref="ProviderBase"/>. /// </summary> /// <typeparam name="T"></typeparam> public class ProviderCollection<T> : ProviderCollection where T : Rigsby.Configuration.Provider.ProviderBase { #region Public Properties /// <summary> /// Gets the <see cref="T:DataProvider"/> with the specified name. /// </summary> /// <value></value> public new T this[string name] { get { return (T)base[name]; } } /// <summary> /// Gets the cref="<T>"/> at the specified index. /// </summary> /// <value></value> public T this[int index] { get { int counter = 0; foreach(T provider in this) { if (counter == index) { return provider; } counter++; } return null; } } #endregion Public Properties #region Public Methods /// <summary> /// Adds a provider to the collection. /// </summary> /// <param name="provider">The provider to be added.</param> /// <permissionSet class="System.Security.permissionSet" version="1"> /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="UnmanagedCode, ControlEvidence"/> /// </permissionSet> public override void Add( System.Configuration.Provider.ProviderBase provider) { if (!(provider is T)) { throw new ArgumentException( String.Format("The provider is not of type {0}.", typeof(T).ToString())); } this.Add((T)provider); } /// <summary> /// Adds the specified provider. /// </summary> /// <param name="provider">The provider.</param> public void Add( T provider) { if (!provider.IsInitialized) { provider.Initialize(); } base.Add(provider); } #endregion Public Methods } }
Creating a Generic ProviderRepository
All of the changes listed above are extensions of the the default provider model to make it easier to use. The ProviderRepository though is a new concept not in the default provider model, but builds on the extensions we have already talked about. The ProviderRepository is designed to encapsulate the loading of providers from the app.config file. It will contain the list of available providers and specify which provider should be used based on the "defaultProvider". It is a generic class that takes in the type of provider you are working with. For most implementations you will probably be extending ProviderBase the, so you will use ProviderRepositoy<MyProviderType>. For example if you have a custom data access layer provider you might call it DataProvider and use ProviderRepository<DataProvider>.
In the ProviderRepository you specify the name of the configuration section to use from the app.config file. If you leave this null or empty, then it will attempt to look for a configuration section with same name as the generic provider type. So if you created it like ProviderRepository<Rigsby.Test.DataAccessLayer>, then it will look for a configuration section named <Rigsby.Test.DataAccessLayer> in the app.config file that is a ProvidersSection.
using System; using System.Configuration; using System.Configuration.Provider; using System.Collections.Generic; using System.Text; using System.Web.Configuration; namespace Rigsby.Configuration.Provider { /// <summary> /// Represents a repository to give access to providers. /// </summary> public class ProviderRepository<T> where T : ProviderBase { #region Private Properties private T m_Provider = null; private ProviderCollection<T> m_Providers = null; private volatile object m_SyncRoot = new object(); private string m_SectionName = String.Empty; #endregion Private Properties #region Public Properties /// <summary> /// Gets the provider. /// </summary> /// <value>The provider.</value> public T Provider { get { if (m_Provider == null && m_Providers.Count > 0) { m_Provider = m_Providers[0]; } return m_Provider; } } /// <summary> /// Gets the providers. /// </summary> /// <value>The providers.</value> public ProviderCollection<T> Providers { get { return m_Providers; } } /// <summary> /// Gets the name of the configuration section. /// </summary> /// <value>The name of the section.</value> public string SectionName { get { return m_SectionName; } } #endregion Public Properties #region Constructors /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> public ProviderRepository( ) : this(true) { } /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> /// <param name="throwErrorIfUnableToLoadSection">if set to <c>true</c> [throw error if unable to load section].</param> public ProviderRepository( bool throwErrorIfUnableToLoadSection) { if (String.IsNullOrEmpty(m_SectionName)) { m_SectionName = typeof(T).ToString(); } LoadProviders(throwErrorIfUnableToLoadSection); } /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> /// <param name="sectionName">Name of the section.</param> public ProviderRepository( string sectionName) : this(sectionName, true) { } /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> /// <param name="sectionName">Name of the section.</param> /// <param name="throwErrorIfUnableToLoadSection">if set to <c>true</c> [throw error if unable to load section].</param> public ProviderRepository( string sectionName, bool throwErrorIfUnableToLoadSection) { m_SectionName = sectionName; LoadProviders(throwErrorIfUnableToLoadSection); } /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> /// <param name="provider">The provider.</param> public ProviderRepository( T provider) { m_Providers = new ProviderCollection<T>(); m_Providers.Add(provider); m_Provider = provider; } /// <summary> /// Initializes a new instance of the <see cref="ProviderRepository<T>"/> class. /// </summary> /// <param name="providers">The providers.</param> public ProviderRepository( ProviderCollection<T> providers) { m_Providers = providers; } #endregion Constructors /// <summary> /// Loads the providers. /// </summary> protected virtual void LoadProviders( bool throwErrorIfUnableToLoadSection) { // Avoid claiming lock if providers are already loaded if (m_Providers == null) { lock (m_SyncRoot) { // Do this again to make sure provider is still null if (m_Providers == null) { // Get the providers m_Providers = new ProviderCollection<T>(); // Get a reference to the section ProvidersSection section = null; try { section = ConfigurationManager.GetSection(m_SectionName) as ProvidersSection; } catch (Exception) { section = null; } if (section == null) { if (throwErrorIfUnableToLoadSection) { throw new ProviderException( String.Format("Unable to load configuration section: '{0}'.", m_SectionName)); } } else { ProvidersHelper.InstantiateProviders(section.Providers, m_Providers, typeof(T)); if (m_Providers.Count > 0) { // If there is a default provider specified, then grab it, // else grab the first provider in the collection if (!String.IsNullOrEmpty(section.DefaultProvider)) { m_Provider = (T)m_Providers[section.DefaultProvider]; } else { m_Provider = m_Providers[0]; } } if (throwErrorIfUnableToLoadSection) { // If we have no provider, then throw an exception if (m_Provider == null) { throw new ProviderException( String.Format("Unable to load default provider for section: '{0}'.", m_SectionName)); } } } } } } } } }
Putting It All Together
To get this all to work you just need to do the following:
- Create the base provider you want to use. This should derive from ProviderBase.
- Created at least one implementation of your base provider. This should derive from the class you made in the previous step.
- Setup a section in your app.config file to specify the providers.
- Create an instance of the ProviderRepository<> in your code that you will use to reference the provider.
Below are examples of these 4 steps to put all of this together.
Step 1 Example:
public abstract class DataProvider : ProviderBase { public abstract System.Data.DataTable GetRows(); public string GetConnectionString() { string connectionStringName = this.Configuration["connectionStringName"]; if (String.IsNullOrEmpty(connectionStringName)) { throw new ProviderException("Empty or missing connectionStringName"); } if (WebConfigurationManager.ConnectionStrings[connectionStringName] == null) { throw new ProviderException("Missing connection string"); } return WebConfigurationManager.ConnectionStrings [connectionStringName].ConnectionString; } }
Step 2 Example:
public class SqlDataProvider : DataProvider { // TODO: Implement data access layer here /// <summary> /// Gets the rows. /// </summary> /// <returns></returns> public override System.Data.DataTable GetRows() { return new System.Data.DataTable(); } }
Step 3 Example:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="DataService" type="Rigsby.Configuration.Provider.ProvidersSection, Rigsby.Configuration.Provider" allowDefinition="MachineToApplication" restartOnExternalChanges="true" /> </configSections> <connectionStrings> <add name="connectionString" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=DB;UID=sa;PWD=;" /> </connectionStrings> <DataService defaultProvider="SqlDataProvider"> <providers> <add name="SqlDataProvider" type="Rigsby.Configuration.Provider.Sample.DataAccessLayer.SqlDataProvider, Rigsby.Configuration.Provider.Sample" connectionStringName="connectionString" providerInvariantName="System.Data.SqlClient" /> </providers> </DataService> </configuration>
Step 4 Example:
public static class Program { private static ProviderRepository<DataAccessLayer.DataProvider> m_DatabaseProviders = new ProviderRepository<DataAccessLayer.DataProvider>("DataService"); public static void Main(string[] args) { Console.WriteLine( String.Format("ProviderName = '{0}'", m_DatabaseProviders.Provider.Name)); Console.WriteLine( String.Format("ConnectionString = '{0}'", m_DatabaseProviders.Provider.GetConnectionString())); Console.ReadLine(); } }
Here is a link to the complete code and samples: http://www.danrigsby.com/files/rigsby.configuration.provider.zip
















