The Stochastic Game
Ramblings of General Geekery

Custom provider attributes in a configuration element (part 1)

A common pattern in .NET is the “provider pattern”, where you have an abstraction for pulling data out of something (a database, a file, your ass, etc.), and one or several implementations of this interface (usually, one for each “something” you can pull data out of).

For this example, we’re going to get cookies (the biscuit, not the browser token, you sad nerd) from some “cookie provider”.

public class Cookie
{
public string Flavor { get; set; }
}
public interface ICookieProvider
{
Cookie ProvideCookie();
}
class Program
{
static void Main(string[] args)
{
ICookieProvider provider = GetCookieProvider();
Cookie cookie = provider.ProvideCookie();
while (cookie != null)
{
Console.WriteLine("Got {0} flavored cookie.", cookie.Flavor);
}
Console.WriteLine("No more cookies.");
}
}

A common way of setting which provider to use is to set this in the configuration file. Let’s create a custom configuration section for this (you need to add System.Configuration as a reference to your project), with a “cookieProvider” element in it.

public class CookieFactoryConfigurationSection : ConfigurationSection
{
[ConfigurationProperty("cookieProvider", IsRequired = true)]
public CookieProviderConfigurationElement CookieProvider
{
get { return this["cookieProvider"] as CookieProviderConfigurationElement; }
set { this["cookieProvider"] = value; }
}
}
public class CookieProviderConfigurationElement : ConfigurationElement
{
[ConfigurationProperty("type", IsRequired = true)]
[TypeConverter(typeof(TypeNameConverter))]
[CallbackValidator(Type = typeof(CookieProviderConfigurationElement), CallbackMethodName = "ValidateProviderType")]
public Type Type
{
get { return this["type"] as Type; }
set { this["type"] = value; }
}
    public static void ValidateProviderType(object type)
{
if (!typeof(ICookieProvider).IsAssignableFrom((Type)type))
{
throw new ConfigurationErrorsException("The cookie provider must implement the ICookieProvider interface.");
}
}
}

We should really use some static ConfigurationProperty members to index our element in the gets/sets, instead of plain strings, but it will do for this example. Also, note the use of the TypeConverter and CallbackValidator attributes, which are pretty neat to let the CLR do all the boring bits to get strongly typed values out of the configuration file. Last, note how we test whether the supplied type implements our interface. This small bit of code has been discussed on other blogs, and I believe this is the best method, until Microsoft decides to add a more straightforward method to System.Type.

Now we can specify the cookie provider in our configuration file:

<configuration>
<configSections>
<section name="cookieFactory" type="ConfigurationTest.CookieFactoryConfigurationSection, ConfigurationTest" />
</configSections>
<cookieFactory>
<cookieProvider type="ConfigurationTest.SimpleCookieProvider, ConfigurationTest" />
</cookieFactory>
</configuration>

And implement the GetCookieProvider method without worrying too much about validating arguments since the configuration API did that for us (it’s a required configuration element):

private static ICookieProvider GetCookieProvider()
{
var section = ConfigurationManager.GetSection("cookieFactory") as CookieFactoryConfigurationSection;
if (section == null)
throw new Exception("No cookie factory found!");
return (ICookieProvider)Activator.CreateInstance(section.CookieProvider.Type);
}

The implementation for the SimpleCookieProvider, used in the configuration file, is, well, simple. It just creates up to 10 cookies.

public class SimpleCookieProvider : ICookieProvider
{
private int mProvidedCookieCount = 0;
#region ICookieProvider Members
public Cookie ProvideCookie()
{
if (mProvidedCookieCount++ > 10)
return null;
return new Cookie() { Flavor = "simple" };
}
#endregion
}

We we run the program, we get the following output:

image

This is all fine, but what if we want to pass some custom (implementation specific) values to initialize our cookie provider? For example, I’d like to specify the maximum amount of cookies to produce, or the flavor for those cookies. Obviously, we can’t do the following:

public interface ICookieProvider
{
void Initialize(int maxCookies, string flavor);
Cookie ProvideCookie();
}

The Initialize method will get very crowded as we create new providers like, for instance, a provider that will allocate a random number of cookies, picking flavors randomly out of an array of strings.

What would be nice would be to declare “freeform” attributes in the XML of the configuration file as such:

<configuration>
<configSections>
<section name="cookieFactory" type="ConfigurationTest.CookieFactoryConfigurationSection, ConfigurationTest" />
</configSections>
<cookieFactory>
<cookieProvider type="ConfigurationTest.SimpleCookieProvider, ConfigurationTest"
maxCookies="5" flavor="chocolate" />
</cookieFactory>
</configuration>

The options would be passed as a collection of name/value pairs, and each cookie provider would be free to do whatever he wants with those options. This is actually what the ProviderBase class does (from the System.Configuration.Provider namespace):

public abstract class ProviderBase
{
protected ProviderBase();
public virtual string Description { get; }
public virtual string Name { get; }
public virtual void Initialize(string name, NameValueCollection config);
}

It gets initialized with a name and a collection of name/value pairs.

We could replace our interface by an abstract class that inherits from ProviderBase, but that’s just too much overhead for our simple project. Let’s just refactor ICookieProvider and SimpleCookieProvider:

public interface ICookieProvider
{
void Initialize(NameValueCollection options);
Cookie ProvideCookie();
}
public class SimpleCookieProvider : ICookieProvider
{
private int mProvidedCookieCount = 0;
private int mMaxCookies = 10;
private string mFlavor = "simple";
#region ICookieProvider Members
public void Initialize(NameValueCollection options)
{
string maxCookiesSetting = options.Get("maxCookies");
if (maxCookiesSetting != null)
mMaxCookies = int.Parse(maxCookiesSetting);
string flavor = options.Get("flavor");
if (flavor != null)
mFlavor = flavor;
}
public Cookie ProvideCookie()
{
if (mProvidedCookieCount++ > mMaxCookies)
return null;
return new Cookie() { Flavor = mFlavor };
}
#endregion
}

Now when we run the program, we get a ConfigurationErrorsException because some unrecognized attributes were found in the configuration file. Getting them to be ignored and stored is easy enough, with the OnDeserializeUnrecognizedAttribute virtual method of the ConfigurationElement class:

public class CookieProviderConfigurationElement : ConfigurationElement
{
public NameValueCollection Options { get; private set; }
[ConfigurationProperty("type", IsRequired = true)]
[TypeConverter(typeof(TypeNameConverter))]
[CallbackValidator(Type = typeof(CookieProviderConfigurationElement), CallbackMethodName = "ValidateProviderType")]
public Type Type
{
get { return this["type"] as Type; }
set { this["type"] = value; }
}
public CookieProviderConfigurationElement()
{
Options = new NameValueCollection();
}
protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
{
Options.Add(name, value);
return true;
}
public static void ValidateProviderType(object type)
{
if (((Type)type).GetInterface(typeof(ICookieProvider).Name) == null)
{
throw new ConfigurationErrorsException("The cookie provider must implement the ICookieProvider interface.");
}
}
}

Let’s not forget to actually call the Initialize method:

private static ICookieProvider GetCookieProvider()
{
var section = ConfigurationManager.GetSection("cookieFactory") as CookieFactoryConfigurationSection;
if (section == null)
throw new Exception("No cookie factory found!");
var provider = (ICookieProvider)Activator.CreateInstance(section.CookieProvider.Type);
provider.Initialize(section.CookieProvider.Options);
return provider;
}

Now when we run the program again, we have the expected output:

image

This is all fine and dandy for simple programs and simple requirements, but it doesn’t work for more elaborate scenarii (yes, I’m the kind of guy that says “scenarii” instead of “scenarios”). For example, what if we had an “Options” dialog that allowed the user to modify the configuration of the program?

Let’s simulate this by hard-coding a change in the configuration:

private static void SimulateConfigurationChange()
{
var configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var section = configuration.GetSection("cookieFactory") as CookieFactoryConfigurationSection;
section.CookieProvider.Options["maxCookies"] = "6";
configuration.Save();
}

This method is called at the end of the program. When we run it, the configuration file isn’t changed.

What’s wrong? We’ll get on that in part 2 of this series!