Polymorphic custom configuration section

742 views Asked by At

Lets say I have the following configuration section....

<yak>
    <whaa hello="world" />
</yak>
<yak>
   <whaa hello="world" blot="1" />
</yak>

Because the second <whaa> element has the extra attribute on it I want to map it to a sub type of the the type that's mapped to the first <whaa> element.

So how do I get polymorphic binding?

2

There are 2 answers

0
Alex Filipovici On BEST ANSWER

Here's one approach by using a programmatic solution which overrides the OnDeserializeUnrecognizedElement method of the ConfigurationSection class. The base and child configuration element classes:

public class Whaa : ConfigurationElement
{
    [ConfigurationProperty("hello", IsRequired = true)]
    public string Hello
    {
        get
        {
            return base["hello"] as string;
        }
        set
        {
            base["hello"] = value;
        }
    }
}

public class WhaaChild : Whaa
{
    [ConfigurationProperty("blot", IsRequired = true)]
    public string Blot
    {
        get
        {
            return base["blot"] as string;
        }
        set
        {
            base["blot"] = value;
        }
    }
}

The configuration section class:

public class Yak : ConfigurationSection
{
    public Whaa MyProperty;

    protected override bool
        OnDeserializeUnrecognizedElement(string elementName, XmlReader reader)
    {
        if (elementName == "whaa")
        {
            try
            {
                var hello = reader.GetAttribute("hello");
                if (hello != null)
                {
                    var blot = reader.GetAttribute("blot");
                    if (blot != null)
                    {
                        MyProperty = new WhaaChild()
                        {
                            Blot = blot,
                            Hello = hello
                        };
                    }
                    else
                    {
                        MyProperty = new Whaa()
                        {
                            Hello = hello
                        };
                    }
                }
            }
            catch (Exception)
            {
                // TODO: add exception handling
            }
        }
        return true;
    }
}

The sample usage:

var yakSectionSettings = (Yak)ConfigurationManager.GetSection("yak");

And the configuration markup:

<configSections>
    <section name="yak" type="Yak, MyApplication"/>
</configSections>

You may use now either:

<yak>
    <whaa hello="world"/>
</yak>

for getting a Whaa object, or:

<yak>
    <whaa hello="world" blot="1"/>
</yak>

for getting a WhaaChild object at runtime.

0
ttremper On

This solution uses a Proxy class to wrap the subtype implementations. There is a comment at the bottom that even refers to naming the collection elements differently for the different subtypes, so along the lines of:

<yak>
    <whaa hello="world" />
</yak>
<yak>
   <whaaChild hello="world" blot="1" />
</yak>

Posting the excerpts below just in case the link dies someday, but please support their blog by trying the link first!

NestedConfiguration.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Xml;

namespace NestedConfiguration
{
  public class CollectionSection : ConfigurationSection
  {
    [ConfigurationProperty("collection", IsDefaultCollection = false)]
    [ConfigurationCollection(typeof(CollectionConfig), AddItemName = "add")]
    public CollectionConfig Collection
    {
      get
      {
        return (CollectionConfig) this["collection"];
      }

      set
      {
        this["collection"] = value;
      }
    }
  }

  public class Parent : ConfigurationElement
  {
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
      get
      {
        return (string) this["name"];
      }

      set
      {
        this["name"] = value;
      }
    }

    [ConfigurationProperty("type", IsRequired = true)]
    public string Type
    {
      get
      {
        return (string) this["type"];
      }

      set
      {
        this["type"] = value;
      }
    }

    public void ProxyDeserializeElement(XmlReader reader, bool serializeCollectionKey)
    {
      DeserializeElement(reader, serializeCollectionKey);
    }
  }

  public class One : Parent
  {
    [ConfigurationProperty("p1")]
    public string P1
    {
      get
      {
        return (string)this["p1"];
      }

      set
      {
        this["p1"] = value;
      }
    }
  }

  public class Two : Parent
  {
    [ConfigurationProperty("p2")]
    public string P2
    {
      get
      {
        return (string)this["p2"];
      }

      set
      {
        this["p2"] = value;
      }
    }
  }

  public class Proxy : ConfigurationElement
  {
    private Parent _Parent = null;

    public Parent Parent
    {
      get
      {
        return _Parent;
      }
    }

    public Proxy()
    {
    }

    public Parent Instance
    {
      get
      {
        return _Parent;
      }
    }

    protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
    {
      string type = reader.GetAttribute("type");
      switch (type)
      {
        case "one":
          _Parent = new One();
          break;
        case "two":
          _Parent = new Two();
          break;
        default:
          throw new ArgumentException(string.Format("Invalid type: {0}", type));
      }

      _Parent.ProxyDeserializeElement(reader, serializeCollectionKey);
    }
  }

  public class CollectionConfig : ConfigurationElementCollection
  {
    public CollectionConfig()
    {

    }

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

    protected override Object GetElementKey(ConfigurationElement element)
    {
      return ((Proxy)element).Parent.Name;
    }

    public Parent this[int index]
    {
      get
      {
        return ((Proxy)BaseGet(index)).Parent;
      }

      set
      {
        if (BaseGet(index) != null)
        {
          BaseRemoveAt(index);
        }

        BaseAdd(index, value);
      }
    }
  }
}

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <configSections>
  <section name="CollectionSection" type="NestedConfiguration.CollectionSection, NestedConfiguration" />
 </configSections>
 <CollectionSection>
  <collection>
   <add type="one" name="one-1" p1="one-1 p1" />
   <add type="one" name="one-2" p1="one-2 p1" />
   <add type="two" name="two-1" p2="two-1 p2" />
  </collection>
 </CollectionSection>
</configuration>

Program.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;

namespace NestedConfiguration
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        ExeConfigurationFileMap map = new ExeConfigurationFileMap();
        map.ExeConfigFilename = "NestedConfiguration.exe.config";
        Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
        CollectionSection config = (CollectionSection)configuration.Sections[typeof(CollectionSection).Name];
        Console.WriteLine("Nested configurations: {0}", config.Collection.Count);
        foreach (Proxy proxy in config.Collection)
        {
          Console.WriteLine("Type: {0}", proxy.Parent.GetType());
        }
      }
      catch(Exception ex)
      {
        Console.WriteLine(ex.Message);
      }
    }
  }
}