Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to make AppSettings work with multiple values for a key

0.00/5 (No votes)
6 Jul 2003 1  
Fixing NameValueSectionHandler using reflection and using it seamlessly.

Sample Image - NameValueMultipleSectionHandler.png

Introduction

The NameValueSectionHandler provided with the .NET framework has a serious flaw: despite of NameValueCollection being able to have multiple values for a given key, when using this handler we get only the last value added for that key.

Since ConfigurationSettings.AppSettings, the easiest standard to read app settings uses this handler in a very particular way, developers usually have to create a custom section and read values using ConfigurationSettings.GetConfig and lots of casts.

We'll hack AppSettings to make our life easier, and learn some interesting things.

Why it doesn't work

The reason it doesn't work the way we would expect lies in a single line. Using a de-compiler (like the great Lutz Roeder's Reflector), we'll find the line that adds new values to the collection from the config files looks like this:

collection[key] = value;

instead of

collection.Add(key, value);

As you may know, the first form replaces the value if the key already exists in the collection.

More problems

To make things worse, the returned collection is actually a ReadOnlyNameValueCollection, a derived class. AppSettings' get method casts the NameValueCollection returned by the handler, and since this type is internal, we can't (directly) instantiate it.

But we are all hardcore developers that won't let some silly thing like that bother us, ain't we?

Creating the new handler

Creating a IConfigurationSectionHandler is easy. We get an XmlNode corresponding to the section in the app.config or web.config file, along with the parent section and the context (which is only important in ASP.NET files).

The code for the Create method is pretty straightforward:

NameValueCollection collection = null;

//Check if we have found the ReadOnlyNameValueCollection Type

if (readOnlyNameValueCollectionType != null)
{
    collection = CreateCollection(parent);
    foreach (XmlNode xmlNode in section.ChildNodes)
    {
        //Skip non-elements like section attributes, comments, etc

        if (xmlNode.NodeType == XmlNodeType.Element)
        {
            switch (xmlNode.Name)
            {
                case "add":
                    collection.Add(xmlNode.Attributes["key"].Value,
                        xmlNode.Attributes["value"].Value);
                    break;
                case "remove":
                    collection.Remove(xmlNode.Attributes["key"].Value);
                    break;
                case "clear":
                    collection.Clear();
                    break;
            }
        }
    }
}
return collection;

Now, the tricky part here is creating a ReadOnlyNameValueCollection. Since it's internal we'll have to create it using reflection.

We'll search for the class and it's constructors in a static method, so it's done only once. In an ASP.NET application, we could use HttpRuntime.Cache to store them and save some extra time (otherwise, this code will get called for each request).

readOnlyNameValueCollectionType = typeof(NameValueCollection).
    Assembly.GetType("System.Configuration.ReadOnlyNameValueCollection");
if (readOnlyNameValueCollectionType != null)
{
    //Get the constructors for the specified parameter types

    readOnlyNameValueCollectionConstructor1 =
        readOnlyNameValueCollectionType.GetConstructor(
        new Type[] {readOnlyNameValueCollectionType});

    readOnlyNameValueCollectionConstructor2 =
        readOnlyNameValueCollectionType.GetConstructor(
        new Type[] {typeof(IHashCodeProvider), typeof(IComparer)});
}

Now, when we need a ReadOnlyNameValueCollection instance, we can use the static CreateCollection method:

if (parent == null)
{
    //Create empty collection

    return
        (NameValueCollection)readOnlyNameValueCollectionConstructor2.Invoke(
        new object[] {
            new CaseInsensitiveHashCodeProvider(
            CultureInfo.InvariantCulture),
            new CaseInsensitiveComparer(
            CultureInfo.InvariantCulture)});
}
else
{
    //Copy parent elements

    return
        (NameValueCollection)readOnlyNameValueCollectionConstructor1.Invoke(
        new object[] {parent});

}

Using the handler

In order to have AppSettings use our handler instead of the default, we have to override the default by removing the appSettings section and creating a new one. Unfortunately, this has to be done for all our apps, but I think it's worth it.

This is a sample app.config file:

<?xml version="1.0" encoding="Windows-1252" ?>
<configuration>
    <configSections>
        <remove name="appSettings" />
        <section name="appSettings"
            type="DigBang.Configuration.NameValueMultipleSectionHandler,
            Configuration" />
    </configSections>
    <appSettings>
        <add key="file" value="myfile1" />
        <add key="file" value="myfile2" />
        <add key="connectionString" value="my connection string" />
        <add key="another multiple values key" value="my value 1" />
        <add key="another multiple values key" value="my value 2" />
        <add key="another multiple values key" value="my value 3" />
    </appSettings>
</configuration>

Now you can use ConfigurationSettings.AppSettings[key] as always, as well as ConfigurationSettings.AppSettings.GetValues(key), which returns a string[] with all the values for that key.

You'll find an example in the demo project that uses both. Keep in mind that if you read a key that has multiple values with ConfigurationSettings.AppSettings[key] instead of GetValues, you'll get a comma-separated string that has all the values concatenated.

Of course you don't have to override the default behavior if you don't want to. You can create your own configSections that use this handler, and read the values with either the ConfigurationSettings.GetConfig method (you'll have to cast), or with the provided NameValueMultipleSectionHandler.GetConfig which returns a NameValueCollection (it will throw an InvalidCastException it you use it with a section whose handler doesn't return a NameValueCollection).

Suggested reading (from the VS.NET documentation)

History

  • 2003-07-07:

    Optimized the type search

  • 2003-06-11:

    First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here