Introduction
This is a simple but useful tip for people who need to create their customized configuration sections. When you are trying to provide your own section in App.config, a little problem, which possibly bothers you, is that the ConfigurationElementCollection
class is not a type-explicit collection. Each element of enumeration you get is an object
as it only implements the interface ICollection
and IEnumerable
. Although it can be casted with the extension method Enumerable.Cast<TResult>()
, the InvalidCastException
can still occur accidentally for other people who are not that familiar with the element’s actual type. The following tip will resolve the problem.
The Walkthrough
Let us say you have a GameConfigurationElement
derived from ConfigurationElement
as below:
namespace Valve.Dota2.Configuration
{
public class GameConfigurationElement : ConfigurationElement
{
#region Properties
[ConfigurationProperty("type", IsRequired = true)]
public string HeroName
{
get { return Convert.ToString(this["hero"]); }
set { this["hero"] = value; }
}
[ConfigurationProperty("description")]
public string Description
{
get { return Convert.ToString(this["description"]); }
set { this["description"] = value; }
}
#endregion
#region Constructor
public GameConfigurationElement()
{
}
#endregion
}
}
and you have a GameConfigurationElementCollection
derived from ConfigurationElementCollection
, representing the collection of GameConfigurationElement
.
namespace Valve.Dota2.Configuration
{
public class GameConfigurationElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new GameConfigurationElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return (element as GameConfigurationElement).HeroName;
}
}
}
Then let us try to make our GameConfigurationElementCollection
type-explicit. First of all, it shall implement IEnumerable<GameConfigurationElement>
as each element within it is supposed to be GameConfigurationElement
instance only.
namespace Valve.Dota2.Configuration
{
public class GameConfigurationElementCollection : ConfigurationElementCollection,
IEnumerable<GameConfigurationElement>
{
#region ConfigurationElementCollection Methods
protected override ConfigurationElement CreateNewElement()
{
return new GameConfigurationElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return (element as GameConfigurationElement).HeroName;
}
#endregion
#region IEnumerable<GameConfigurationElement> Methods
public new IEnumerator<GameConfigurationElement> GetEnumerator()
{
throw new NotImplementedException();
}
#endregion
}
}
How do we implement the IEnumerator<GameConfigurationElement> GetEnumerator()
method? One of the solutions you may think of is probably like this (at least I did before):
public new IEnumerator<GameConfigurationElement> GetEnumerator()
{
return this.Cast<GameConfigurationElement>().GetEnumerator();
}
But the only outcome you will get during the enumeration (for instance, a foreach
loop) is:
How come? Because both Enumerable.Cast<TResult>()
and Enumerable.AsEnumerable<TResult>()
extensions will try to call IEnumerator<TResult> GetEnumerator()
to get the actual result. Since your collection has implemented the interface, it will result in StackOverflowException
whenever you try to cast your collection to specified type within the method itself.
To get rid of this exception, we need to make a detour to avoid casting the collection directly. Here is one of the feasible solutions by utilizing Enumerable.Range
and ConfigurationElementCollection.BaseGet(Int32)
method to Select
each element individually. After that, we "yield" the loop, as below:
public new IEnumerator<GameConfigurationElement> GetEnumerator()
{
foreach (GameConfigurationElement element in
Enumerable.Range(0, base.Count).Select(base.BaseGet))
yield return element;
}
so the problem gets resolved. We are now able to safely enumerate the element of our collection (and of course, it is type-explicit!):
using System;
using System.Configuration;
namespace Valve.Dota2.Configuration.Sample
{
class Program
{
static void Main(string[] args)
{
GameConfigurationSection section =
ConfigurationManager.GetSection("valve.dota2") as GameConfigurationSection;
foreach (var hero in section.Games)
{
Console.WriteLine("Hero Name: {0}", hero.HeroName);
Console.WriteLine("Hero Description: {0}", hero.Description);
Console.WriteLine();
}
Console.ReadKey(true);
}
}
}
The GameConfigurationSection
class can be seen below:
using System.Collections.Generic;
using System.Configuration;
namespace Valve.Dota2.Configuration
{
public class GameConfigurationSection : ConfigurationSection
{
[ConfigurationProperty("games")]
[ConfigurationCollection(typeof(GameConfigurationElementCollection),
AddItemName = "add",
ClearItemsName = "clear",
RemoveItemName = "remove")]
public GameConfigurationElementCollection Games
{
get { return this["games"] as GameConfigurationElementCollection; }
set { this["games"] = value; }
}
}
}
and here is our App.confg.
="1.0"="utf-8"
<configuration>
<configSections>
<section name="valve.dota2"
type="Valve.Dota2.Configuration.GameConfigurationSection, Valve.Dota2.Configuration"/>
</configSections>
<valve.dota2>
<games>
<add hero="Invoker" description="No more midas!"></add>
<add hero="Techies" description="Deserves to be nerfed."></add>
<add hero="Templar Assasin" description="My waifu."></add>
</games>
</valve.dota2>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
References
History
- 2015-10-09 Initial post
- 2015-10-09 Added attachment link,
GameConfigurationSection
and App.config detail