Click here to Skip to main content
Click here to Skip to main content

How to make AppSettings work with multiple values for a key

, 6 Jul 2003
Rate this:
Please Sign up or sign in to vote.
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

Share

About the Author

Diego Mijelshon
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
QuestionDoes it work in .Net 2.0+? PinmemberPerlDev16-Sep-07 16:38 
AnswerRe: Does it work in .Net 2.0+? PinmemberDiego Mijelshon24-Sep-07 7:29 
GeneralRe: Does it work in .Net 2.0+? Pinmemberjamgray9-Nov-07 7:30 
GeneralRe: Does it work in .Net 2.0+? Pinmemberjamgray9-Nov-07 7:48 
GeneralRe: Does it work in .Net 2.0+? PinmemberJuneau18-Jun-08 10:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141015.1 | Last Updated 7 Jul 2003
Article Copyright 2003 by Diego Mijelshon
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid