26 January 2011

Adding collections to a custom ConfigurationSection

The attributed model for creating custom ConfigurationSection types for use in your app.config or web.config file is quite verbose and examples are hard to come by. Collections in particular are a pain point, there is very little documentation around them and the examples all tend to follow the default add/remove/clear model i.e. that used in <appSettings/>.

Three particular scenarios with collections which caused me problems while doing the same piece of work were:

  • When the items of a collection have a custom name e.g. "item" instead of add/remove/clear
  • When the items of a collection can have different element names representing different actions or subclasses e.g. the  <allow/> and <deny/> elements used with <authorization/>
  • When the items of a collection don’t have an attribute which represents a unique key e.g. not having anything like the key attribute of an <add/> or <remove/> element

This first and last are relatively trivial to fix, the second less so and it took me a bit of digging around in Reflector to work out how to set up something that worked.

Collection items with a custom element name

This scenario can be accomplished as follows.


public class MySpecialConfigurationSection : ConfigurationSection
{
 [ConfigurationProperty("", IsRequired = false, IsKey = false, IsDefaultCollection = true)]
 public ItemCollection Items
 {
  get { return ((ItemCollection) (base["items"])); }
  set { base["items"] = value; }
 }
}

[ConfigurationCollection(typeof(Item), CollectionType = ConfigurationElementCollectionType.BasicMapAlternate)]
public class ItemCollection : ConfigurationElementCollection
{
 internal const string ItemPropertyName = "item";

 public override ConfigurationElementCollectionType CollectionType
 {
  get { return ConfigurationElementCollectionType.BasicMapAlternate; }
 }

 protected override string ElementName
 {
  get { return ItemPropertyName; }
 }

 protected override bool IsElementName(string elementName)
 {
  return (elementName == ItemPropertyName);
 }

 protected override object GetElementKey(ConfigurationElement element)
 {
  return ((Item)element).Value;
 }

 protected override ConfigurationElement CreateNewElement()
 {
  return new Item();
 }

 public override bool IsReadOnly()
 {
  return false;
 }

}

public class Item
{
 [ConfigurationProperty("value")]
 public string Value 
 {
  get { return (string)base["value"]; }
  set { base["value"] = value; }
 }
}

Which will allow us to specify our section like so:


<configSections>
  <section name="mySpecialSection" type="MyNamespace.MySpecialConfigurationSection, MyAssembly"/> 
</configSections>

...

<mySpecialSection>
 <item value="one"/>
 <item value="two"/>
 <item value="three"/>
</mySpecialSection>

First off we have a property representing our collection on our ConfigurationSection or ConfigurationElement whose type derives from ConfigurationElementCollection. This property decorated with a ConfigurationProperty attribute. If the collection should be contained directly within the parent element then set IsDefaultCollection equal to true and leave element name as empty string. If the collection should be contained within a container element specify an element name.

Next, the ConfigurationElementCollection derived type of the property should have a ConfigurationCollection attribute specifying element type and collection type. The collection type specifies the inheritance behaviour when the section appears in web.config files nested deeper in the folder structure for example.

For the collection type itself we do this:

  • Override ElementName to return collection item element  name
  • Override IsElementName to return true when encountering element name
  • Override GetNewElement() to new up an instance of your item type
  • Override GetElementKey(element) to return an object which uniquely identifies the item. This could be a property value, a combination of values as some hash or the element itself

Collection items with varying element name


public class MySpecialConfigurationSection : ConfigurationSection
{
 [ConfigurationProperty("items", IsRequired = false, IsKey = false, IsDefaultCollection = false)]
 public ItemCollection Items
 {
  get { return ((ItemCollection) (base["items"])); }
  set { base["items"] = value; }
 }    
}
    
[ConfigurationCollection(typeof(Item), AddItemName = "apple,orange", CollectionType = ConfigurationElementCollectionType.BasicMapAlternate)]
public class ItemCollection : ConfigurationElementCollection
{
 public override ConfigurationElementCollectionType CollectionType
 {
  get { return ConfigurationElementCollectionType.BasicMapAlternate; }
 }

 protected override string ElementName
 {
  get { return string.Empty; }
 }

 protected override bool IsElementName(string elementName)
 {
  return (elementName == "apple" || elementName == "orange");
 }

 protected override object GetElementKey(ConfigurationElement element)
 {
  return element;
 }

 protected override ConfigurationElement CreateNewElement()
 {
  return new Item();
 }

 protected override ConfigurationElement CreateNewElement(string elementName)
 {
  var item = new Item();
  if (elementName == "apple")
  {
   item.Type = ItemType.Apple;
  }
  else if(elementName == "orange")
  {
   item.Type = ItemType.Orange;
  }
  return item;
 }
 
 public override bool IsReadOnly()
 {
  return false;
 }
}

public enum ItemType
{
 Apple,
 Orange
}

public class Item
{
 public ItemType Type { get; set; }

 [ConfigurationProperty("value")]
 public string Value 
 {
  get { return (string)base["value"]; }
  set { base["value"] = value; }
 }
}

Which will allow us to specify our section like so:


<configSections>
  <section name="mySpecialSection" type="MyNamespace.MySpecialConfigurationSection, MyAssembly"/> 
</configSections>

...

<mySpecialSection>
 <items>
  <apple value="one"/>
  <apple value="two"/>
  <orange value="one"/>
 </items>
</mySpecialSection>

Notice that here we've specified two collection items with the value "one" which would have resulted in one overwriting the other in the previous example. To get around this, instead of returning the Value property we're returning the element itself as the unique key.

This time our ConfigurationElementCollection derived type's ConfigurationCollection attribute also specifies a comma delimited AddItemName e.g. "allow,deny". We override the methods of the base as follows:

  • Override ElementName to return empty string
  • Override IsElementName to return true when encountering a correct element name
  • Override GetNewElement() to new up an instance of your item type
  • Override GetNewElement(elementName) to new up an instance of the correct item type for particular element name setting relevant properties
  • Override GetElementKey(element) to return an object which uniquely identifies the item. This could be a property value, a combination of values as some hash or the element itself

Caveat

While our varying element names will be readable the object model is read-only. I haven't covered support for writing changes back to the config file here as it involves taking charge of the serialization of the objects so really requires its own blog post.

Links

5 comments:

Anonymous said...

Thanks for publishing your code.
I'm trying to implement first part ("Collection items with a custom element name") and cannot compile it as-is.
I can compile it if I do the following:
public class Item : ConfigurationElement
{...}
...but then get a run-time exception:
"The property 'Items' must not return null from the property's get method. Typically the getter should return base[\"\"]."

What am I doing wrong?

Derek Fowler said...

Its talking about the Items property - you need to do something like:

return (ItemsCollection)base["items"];

in your getter. Where ItemsCollection is your collection type.

Anonymous said...

I had the same problem. I solved it by using propertyName == "" instead of "items".

Mandar Patki said...

Thanks for the code, but ItemType defintion is missing from the code, can you please paste that?

Derek Fowler said...

ItemType was just an enum - have added it to the example code.