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;
if (readOnlyNameValueCollectionType != null)
{
collection = CreateCollection(parent);
foreach (XmlNode xmlNode in section.ChildNodes)
{
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)
{
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)
{
return
(NameValueCollection)readOnlyNameValueCollectionConstructor2.Invoke(
new object[] {
new CaseInsensitiveHashCodeProvider(
CultureInfo.InvariantCulture),
new CaseInsensitiveComparer(
CultureInfo.InvariantCulture)});
}
else
{
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:
="1.0" ="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