Oct 29 2008

I'll be your Provider

When .NET 2.0 was released the Provider model was introduced, and many core .NET features rely on this pattern and/or provide extensibility to you via this pattern. If you are not familiar with it, it is essentially a type of Factory pattern. Implemented correctly this can provide thread-safe isolation of Services and Repositories to coin some Domain Driven Design (DDD) terms. While not a new pattern, it is still surprising how seldom this is used, and badly some implementations are. Here is a more official description:

The provider model is a design pattern formulated by Microsoft for use in the ASP.NET Starter Kits and formalized in .NET version 2.0. It is used to allow an application to choose from one of multiple implementations in the application configuration, for example, to provide access to different data stores to retrieve login information, or to use different storage methodologies such as a database, binary to disk, XML, etc.
- Wikipedia (http://en.wikipedia.org/wiki/Provider_model)

With that said, it is better understood with a walk-through so lets go through a very simple example that can easily be extended to meet more specific needs. Here are the parts we will need to achieve this:

  • An interface defining the contract all concrete implementations must implement to be registered as a Provider
  • A sample concrete implementation of the Provider interface
  • A collection to maintain all registered Provider instances
  • A static Factory to facilitate access to registered Providers
  • Configuration resources to facilitate registration of new/replacement Providers

First the interface. This should represent whatever you need to fulfill your business requirements:

public interface ILogProvider
{
    void LogRequest(string request);
}

Next we need a concrete implementation of the interface, this must implement both the interface we defined above and the Provider base class (e.g. System.Configuration.Provider.ProviderBase). The base class will help take care of some of the Magic that makes the Provider model so easy to use:

public sealed class LogProvider : ProviderBase, ILogProvider
{
    public override void Initialize(string name, NameValueCollection config)
    {
        if (string.IsNullOrEmpty(name))
            throw new ConfigurationErrorsException("The LogProvider's 'name' attribute was not set. Setting the 'name' attribute is required.");

        if (config == null)
            config = new NameValueCollection();

        // Initialize the abstract base class
        base.Initialize(name, config);
    }

    #region ILogProvider Members

    public void LogRequest(string request)
    {
        throw new NotImplementedException();
    }

    #endregion
}

When we start registering Providers, we will need a collection object to maintain them. Since Providers require specialized handling, .NET has provided us one just for this purpose. What we need to do is strongly type it for our purposes.

public sealed class LogProviderCollection : ProviderCollection
{
    public new ILogProvider this[string name]
    {
        get
        {
            if (string.IsNullOrEmpty(name))
                return null;

            try
            {
                return (ILogProvider)base[name];
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("The requested provider could not be loaded because it either does not exist or is the wrong type. Name: {0}", name), ex);
            }
        }
    }
}

Now we need the factory that exposes registered Providers to callers. This needs to create and register all configured Providers and facilitate calls to them.

public static class Loggers
{
    private static readonly object SyncRoot = new object();

    static Loggers()
    {
        Initialize();
    }

    public static ILogProvider Provider { get; private set; }

    public static LogProviderCollection Providers { get; private set; }

    private static void Initialize()
    {
        lock (SyncRoot)
        {
            MyCustomConfigSection configuration = MyCustomConfigSection.Current;

            if (configuration == null)
                throw new ConfigurationErrorsException(
                    string.Format(CultureInfo.InvariantCulture,
                    "The '{0}' configuration element in the '{1}' configuration section is not set correctly.",
                    MyCustomConfigSection.SectionName,
                    MyCustomConfigSection.LogProvidersPropertyName));

            Providers = new LogProviderCollection();

            ProvidersHelper.InstantiateProviders(configuration.Loggers.Providers, Providers,
                typeof(ILogProvider));

            Providers.SetReadOnly();

            Provider = Providers[configuration.Loggers.DefaultProvider];

            if (Provider == null)
                throw new ConfigurationErrorsException("defaultProvider is not valid");
        }
    }
}

Now we have all the main components but this will not work without the ability to configure/register Provider instances. After all, this is a Factory. With that in mind, we need a config section and related configuration elements. Again, .NET has provided us with some assistance, all we need is to Type and name things to suite our needs.

public sealed class MyCustomConfigSection : ConfigurationSection
{
    #region Loggers Property

    internal const string LogProvidersPropertyName = "loggers";

    [ConfigurationProperty(LogProvidersPropertyName)]
    public ProviderConfigElement Loggers
    {
        get { return (ProviderConfigElement)this[LogProvidersPropertyName]; }
    }

    #endregion

    #region Singleton Instance

    internal const string SectionName = "custom";

    public static MyCustomConfigSection Current
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    #endregion
}

Here is how you register logger implementations. This Factory clearly separates instancing from the caller, and allows you to register new loggers externally. A good practice is to support setting a default which allows the caller to be even further removed from which one is used.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="custom" type="MyAssembly.MyCustomConfigSection, MyAssembly"/>
    </configSections>
    <custom>
        <loggers>
            <providers "text-decoration: underline;">defaultProvider="SampleLogger">
                <clear />
                <add name="SampleLogger" type="MyAssembly.LogProvider, MyAssembly" />
                <add name="AnotherLogger" type="MyAssembly.AnotherLogger, MyAssembly" />
                <add name="YetAnotherLogger" type="MyAssembly.YetAnotherLogger, MyAssembly" />
            </providers>
        </loggers>
    </custom>
</configuration>
  

With everything configured we can now start using the Factory.  There are two ways to use it.  The first is to use the default Provider and the second is to call a Provider by its configured name.

Loggers.Provider.LogRequest("Log this");

Loggers.Providers["AnotherLogger"].LogRequest("Log this");

So that's it, but there is one more pattern that can be used with Providers. Let assume you want multiple Provider instances to receive a call without the caller having to loop through the collection. To accomplish this, we need to add a new proxy method to the Factory, but we also need to make sure this is done in a thread-safe manner.

public static void LogRequest(string request)
{
    lock (SyncRoot)
    {
        foreach (ILogProvider provider in Providers)
        {
            provider.LogRequest(request);
        }
    }
}

To use it, all we need to do is call the proxy method.

Loggers.LogRequest("Log this");

That's all there is... Keep in mind that since these are registered in a proper Configuration Section you can take advantage of configuration cascading and make Providers available to more than one Application at a time in the same way that HttpModules, and HttpHandlers are cascaded.  Hope you find this useful.

Tags:

Comments

1.
trackback DotNetKicks.com says:

I'll be your Provider

You've been kicked (a good thing) - Trackback from DotNetKicks.com

2.
trackback Smelser.NET says:

Is my code Fluent?

Is my code Fluent?

Comments are closed