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

By , 6 Jul 2003
 

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

About the Author

Diego Mijelshon
Web Developer
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralThank you!memberMember 17056234 Nov '09 - 16:41 
Thanks a lot! The article is very usefull for me.
QuestionDoes it work in .Net 2.0+?memberPerlDev16 Sep '07 - 16:38 
For .Net 1.1, it works great. But for 2.0+, it seems not.
AnswerRe: Does it work in .Net 2.0+?memberDiego Mijelshon24 Sep '07 - 7:29 
You are right.
 
The Configuration namespace has changed a lot from 1.x to 2.0.
I might rewrite this article for 2.x if I find the time.
 

GeneralRe: Does it work in .Net 2.0+?memberjamgray9 Nov '07 - 7:30 
The constructor for ReadOnlyNameValueCollection that used to take IComparer and IHashCodeProvider now takes IEqualityComparer. At the moment I'm looking for a .NET class that implements IEqualityComparer (either the generic or non-generic version) that can be easily created to pass to the constructor. If it weren't for that issue, I think it would work fine.
GeneralRe: Does it work in .Net 2.0+?memberjamgray9 Nov '07 - 7:48 
Ok, it looks like we can just use the static properties exposed by the abstract StringComparer class to provide the necessary IEqualityComparer to our ReadOnlyNameValueCollection constructor. The code changes here should do the trick:
 
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(IEqualityComparer) });
}
I chose to do a case-insensitive comparison according to rules of the invariant culture, but there are other options:
 
if (parent == null)
{
    //Create empty collection
    return
        (NameValueCollection)readOnlyNameValueCollectionConstructor2.Invoke(
        new object[] { StringComparer.InvariantCultureIgnoreCase });
}
else
{
    //Copy parent elements
    return
        (NameValueCollection)readOnlyNameValueCollectionConstructor1.Invoke(
        new object[] {parent});
 
}
I hope this helps!
 
-Ryan
GeneralRe: Does it work in .Net 2.0+?memberJuneau18 Jun '08 - 10:30 
Hmmm. Using your demo code, this just returns the (1) last value set in the config. Just like the normal config does.
GeneralNice fixmemberMichael Sadlon17 May '07 - 13:29 
I was also surprised when the String array .NET returned to me....was an array of size one D'Oh! | :doh: Why can't it work the way we all expect it to? If I wanted one value, I'd get index zero! Laugh | :laugh:
 
This fix looks great, and I'm going to give it a try. Thanks!
 

 
On second thought, I only have one overloaded key so this solution is much easier for me Smile | :) Still, great job.
 
VB.Net

Dim arrPrinterFilter As String()
Dim strTempList As String
 
'Read the list from web.config, and split it
strTempList = System.Configuration.ConfigurationSettings.AppSettings.GetValues("PrinterList")(0)
arrPrinterFilter = strTempList.Split(",")

 
Cheers.
GeneralThanks!memberEmma Burrows16 Mar '06 - 22:43 
I was scratching my head wondering why I couldn't retrieve multiple values for the same key name and a quick Google brought up your article. After a little faffing around with namespaces, it works like a charm!
 
Thanks for sharing...
GeneralRe: Thanks!memberDiego Mijelshon17 Mar '06 - 1:21 
I'm glad to help Smile | :)
That was one of my first .NET articles a long time ago, and it's nice to see it's still useful.
 
Diego
GeneralUsing it without building a librarymemberSimon Siwik30 Jan '04 - 2:14 
Well done, a very nice good. It took time to me to fully understand the tricks. I tried the demo prj and that's ok, but...
 
Let's suppose i don't want to build up a class with the Notify... class, instead i want to embed that class in my project.
Well, i tried, but I can't make it works. I suppose when I refer to ConfigurationSettings it hooks up to the original.
 
how can I go through this and make it work?
 
thanks
Simone
 
Italy

GeneralAppSettings writermemberPinx11 Dec '03 - 2:05 
Any ideas on how to make an AppSettings writer? I know of a few solutions, but they all come down to writing your own XML parser etc. I think your solution is much cleaner.
GeneralRe: AppSettings writermemberDiego Mijelshon11 Dec '03 - 2:20 
Check out IConfigurationSectionHandlerWriter in the Configuration Management Application Block[^]
 
    Diego
GeneralI am getting error messagemembersusree11 Sep '03 - 1:26 
I am getting error messages when I am using this configuration setting in my web.config file.
To be more clear I want to use multiple values from a single key in web.config file.I have done the way you have created the demo project for AppSettings.
so please give some suggestions.
thanks in advance
GeneralRe: I am getting error messagememberDiego Mijelshon15 Sep '03 - 11:19 
Please provide the text of the error messages and your web.config file
GeneralI'm getting an exceptionsussDrew Berkemeyer28 Aug '03 - 9:04 
Thank you so much for posting this. I have been blown away by the lack of posts pointing out this bug. Even going to msdn.microsoft.com doesn't review this in the knowledgebase.
 
Anyway, I have incorporated your class, but changed the namespace to be:
LeadImportTool.Configuration.NameValueMultipleSectionHandler.
 
However, when I try to access the class I get an exception:
Could not create LeadImportTool.Configuration.NameValueMultipleSectionHandler
 
Here is my code:
NameValueCollection nameValueCollection = (NameValueCollection)System.Configuration.ConfigurationSettings.GetConfig("categoryTypes");
 
Here are my config entries:
	<configSections>
		<section name="categoryTypes" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
		<sectionGroup name="fieldCategories">
			<section name="ContactFields" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
			<section name="NameFields" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
		</sectionGroup>
		<section name="formatFields" type="System.Configuration.DictionarySectionHandler"/>
	</configSections>
 
Any thoughts?
GeneralRe: I'm getting an exceptionsussDrew Berkemeyer28 Aug '03 - 9:06 
Sorry. I must have goofed.
 
<b>Here</b> are my config settings:
 
     <configSections>
          <section name="categoryTypes" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
          <sectionGroup name="fieldCategories">
               <section name="ContactFields" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
               <section name="NameFields" type="LeadImportTool.Configuration.NameValueMultipleSectionHandler, Configuration"/>
          </sectionGroup>
          <section name="formatFields" type="System.Configuration.DictionarySectionHandler"/>
     </configSections>

GeneralRe: I'm getting an exception [modified]membereslamspider6 Aug '06 - 8:48 
I am using the demo and I'm getting an exception sayes "Configuration system failed to initialize" waht should I do please I need it very much
 
Eslam
 

 
-- modified at 15:01 Sunday 6th August, 2006
GeneralRe: I'm getting an exceptionmembersayarthant28 Nov '07 - 19:54 
I am also getting the same error message. "Configuration system failed to initialize".
please help me.
I created class library as "Configuration" and reference it from my window application.
 
<section name="appSettings" type="MyTest.Configuration.NameValueMultipleSectionHandler, Configuration" />
 
<appSettings>
      <add key="DBTYPE" value="MS ACCESS" />
      <add key="DBTYPE" value="SQL Server" />
</appSettings>
GeneralNevermind, I figured it outsussDrew Berkemeyer28 Aug '03 - 9:21 
I am fairly new to .NET. I didn't realize that the ", Configuration" after your handler declaration was indicating that you were accessing a class library named Configuration. Once I changed it to my app name it worked great.
 
Thank you again for posting this. I'm sure I would have spent many hours attempting to figure out what was going on.
 
- Drew
GeneralRe: Nevermind, I figured it outmemberDiego Mijelshon1 Sep '03 - 10:42 
I'm glad it helped Smile | :)
 
Regarding the ", Configuration" issue, yeah, the full-type-name format looks a little awkward the first times you see it.
It's easier (but less powerful) in Java, where the package name indicates the location of the class...
GeneralNice!memberIan Giffen7 Aug '03 - 17:53 
Thank you very much!
 
This is exactly what I needed to do.
Nice article.
 
Only one thing I would say (positive criticism that all programmers (myself included) will hear until the end of time).....
your source could use a few comments... although most of what you have in the code is displayed here.
 
Thanks again and nice job!
 
Ian Giffen
QuestionWhy?membersnkscore19 Jun '03 - 5:51 
I don't get what this gets you.
 
There is a good deal to be learned from this article, but in the end, why are you any better off?
 
If doing all this allows your code to turn:


 
Into a string = "my value 1, my value2"
 

then why not setup your config file like
 

 

I was execting you to get an array, or a collection out of this.
 
Not knocking the article, lots of good info, but not sure why this helps things.
AnswerRe: Why?memberDiego Mijelshon19 Jun '03 - 9:11 
I think you didn't get the idea, which is probably my fault for giving a bad example.
 
You DO get a string array.
If you look at the example, you will see I do
foreach(string value in ConfigurationSettings.AppSettings.GetValues(key))
where value takes the value of each value under the key "another multiple values key".
 
This is MUCH better than writing <add key="another multiple values key" value="myvalue1, myvalue2">, because a Split() would fail if there are embedded commas.

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 7 Jul 2003
Article Copyright 2003 by Diego Mijelshon
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid