The Stochastic Game
Ramblings of General Geekery

Custom provider attributes in a configuration element (part 2)

In the first part of this little series, we implemented a simple, read-only way to get custom attributes from a configuration element, using a provider pattern use case. We ended trying to modify the configuration file, without much success.

Right now, we have the following method, called at the end of the program:

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 doesn't change anything to the configuration file, even though we're calling the Save method. This is because .NET's configuration is kind of smart, and realizes that nothing has changed, therefore nothing needs to be written. How can it be that nothing has changed, you say, since we modified the Options collection? Oh, but this collection can't be "seen" by the Configuration class because it's just a local property, unlike the other one, Type, who's a property wrapper around ConfigurationElement's internal property system:

public NameValueCollection Options { get; private set; }

[ConfigurationProperty("type", IsRequired = true)]
// Validation/conversion attributes removed for readability...
public Type Type
{
    get { return this["type"] as Type; }
    set { this["type"] = value; }
}

Even if we force the configuration to save itself, using some overloads of the Save method, we end up with a configuration file that lost its custom attributes, leaving only the provider type attribute, because the configuration doesn't know about those options.

We can't turn Options into a ConfigurationProperty, though, because it would mean that we had a nested collection inside our "cookieProvider" XML element. We don't want that. We want to dynamically add new properties to ConfigurationElement.

Looking at the ConfigurationElement class, we can spot something promising: there's a virtual property called Properties which, it seems, contains the element's configuration properties. We can dynamically add items in it:

public class CookieProviderConfigurationElement : ConfigurationElement
{
    // Properties...
    // Constructor...

    protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
    {
        if (!Properties.Contains(name))
            Properties.Add(new ConfigurationProperty(name, typeof(string), null));
        this[name] = value;
        Options[name] = value;
        return true;
    }

    // ValidateProviderType...
}

Now if we force a configuration save, we keep our custom attributes!

private static void SimulateConfigurationChange()
{
    var configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    var section = configuration.GetSection("cookieFactory") as CookieFactoryConfigurationSection;
    section.CookieProvider.Options["maxCookies"] = "6";
    configuration.Save(ConfigurationSaveMode.Minimal, true);    // Force a minimal configuration change
}
<configuration>
  <configSections>
    <section name="cookieFactory" type="ConfigurationTest.CookieFactoryConfigurationSection, ConfigurationTest" />
  </configSections>
  <cookieFactory>
    <cookieProvider type="ConfigurationTest.SimpleCookieProvider, ConfigurationTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
      maxCookies="5" flavor="chocolate" />
  </cookieFactory>
  <system.web>
    <trust level="Full" />
    <webControls clientScriptsLocation="/aspnet_client/{0}/{1}/" />
  </system.web>
</configuration>

We get a bit of additional stuff by forcing a save, but that doesn't matter.

However, we notice that the "maxCookie" parameter is still "5", even though we set it to "6". Well, that's no surprise given that, once again, we set that value on the local Options property that's unknown to the configuration API. We have to somehow modify the internal Properties collection when we modify the options.

At this point, I should let the guy in the audience who's been raising his hand since the beginning ask his question: "what about the ProviderSettings class?". Well, the ProviderSettings is actually almost what we want. It has a NameValueCollection property called Parameters, and if you modify it, you get a correctly modified configuration when you save. However, it has hard coded, required, "name" and "type" configuration properties, which means that if your base provider class has more or less stuff, you'll have to write your own ConfigurationElement class from scratch anyway (inheriting from ProviderSettings won't help because you'll end up with your other "standard" configuration properties in the options collection, which might be confusing).

Besides, isn't it fun to reinvent the wheel to learn how it works? Let's resume our investigations, then.

Now we have a few different choices:

  • We could write a ConfigurationOptionCollection class with a similar interface to that of NameValueCollection. This class would be linked to a ConfigurationElement, and would keep its items in sync with the element's Properties collection. The problem with this approach is that the Properties property is protected, so we would need the ConfigurationElement to also implement an interface or inherit another abstract class that would give our ConfigurationOptionCollection a way to manipulate it. We would also need that interface or abstract class to give a way to prevent illegal actions, like adding an option that has the same name of a regular configuration property (in our case, for example, adding an option called "type", which would collide with the provider type required property). That's 2 additional classes, and some complexity to keep things in sync. Since most use cases for configuration are either read-only, or read and then write in 2 separate, one-shot, operations, we will rarely need to keep things in sync.
  • We could create an abstract subclass to ConfigurationElement that would keep track of options for us. Every time the configuration properties are accessed, the NameValueCollection for the options is rebuilt. This is how the ProviderSettings works.

Here, I went with a simpler version of the second choice, where I replaced the Options property with a couple of methods. This is purely so there's minimal code to make it work, and we can see what are the important bits. Introducing the LooseConfigurationElement (yeah, I'm bad with names):

public abstract class LooseConfigurationElement : ConfigurationElement
{
    protected LooseConfigurationElement()
    {
    }

    public void SetOption(string name, string value)
    {
        if (!this.Properties.Contains(name))
        {
            ConfigurationProperty optionProperty = new ConfigurationProperty(name, typeof(String), null);
            this.Properties.Add(optionProperty);
        }
        this[name] = value;
    }

    public NameValueCollection GetOptions()
    {
        NameValueCollection options = new NameValueCollection();
        foreach (ConfigurationProperty property in this.Properties)
        {
            if (IsOptionProperty(property))
            {
                options.Add(property.Name, this[property.Name] as string);
            }
        }
        return options;
    }

    protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
    {
        SetOption(name, value);
        return true;
    }

    protected abstract bool IsOptionProperty(ConfigurationProperty property);
}

And the refactored CookieProviderConfigurationElement:

public class CookieProviderConfigurationElement : LooseConfigurationElement
{
    private static ConfigurationProperty sTypeProperty =
        new ConfigurationProperty(
            "type",
            typeof(Type),
            null,
            ConfigurationPropertyOptions.IsRequired);

    [ConfigurationProperty("type", IsRequired = true)]
    [TypeConverter(typeof(TypeNameConverter))]
    [CallbackValidator(Type = typeof(CookieProviderConfigurationElement), CallbackMethodName = "ValidateProviderType")]
    public Type Type
    {
        get { return this[sTypeProperty] as Type; }
        set { this[sTypeProperty] = value; }
    }

    public static void ValidateProviderType(object type)
    {
        if (!typeof(ICookieProvider).IsAssignableFrom((Type)type))
        {
            throw new ConfigurationErrorsException("The cookie provider must implement the ICookieProvider interface.");
        }
    }

    protected override bool IsOptionProperty(ConfigurationProperty property)
    {
        if (property == sTypeProperty)
            return false;
        return true;
    }
}

The LooseConfigurationElement is weak in several aspects, like for example the fact that it's not thread-safe. But it fulfills our requirements, and you can see how we can easily play with which configuration properties are "standard" and which ones are "options" through the IsOptionProperty method. This is a small but useful improvement over the ProviderSettings class, which is only written for the ProviderBase class.

You can improve on the LooseConfigurationElement pretty easily:

  • First, you might want to not rebuild the Options collection every time a client asks for it. There are only 2 ways for this collection to be modified: a client modifies it directly, or somebody adds optional properties to the internal Properties collection. Since this second situation only happens in OnDeserializeUnrecognizedAttribute, we can say that we, in fact, almost never have to rebuild the Options collection!
  • However, it also means that we need to keep the internal Properties collection in sync when people modify the Options collection. Since NameValueCollection doesn't have any dirty flag, you can either create your own with this feature, use something like ObservableCollection instead, or go down the ProviderSettings route who partially rebuilds the Properties collection every time it is accessed. In this case, we're almost trading rebuilding one collection for rebuilding another... Also, you'll have to juggle with options already added as ConfigurationProperties, new ones that are only in name/value form, and those that have been removed. But you're smart, you can do that.

Well that's it, we've got all the info we need. I hope it was useful for some of you!