How to implement a ConfigurationSection with a ConfigurationElementCollection

162,683

Solution 1

The previous answer is correct but I'll give you all the code as well.

Your app.config should look like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="ServicesSection" type="RT.Core.Config.ServiceConfigurationSection, RT.Core"/>
   </configSections>
   <ServicesSection>
      <Services>
         <add Port="6996" ReportType="File" />
         <add Port="7001" ReportType="Other" />
      </Services>
   </ServicesSection>
</configuration>

Your ServiceConfig and ServiceCollection classes remain unchanged.

You need a new class:

public class ServiceConfigurationSection : ConfigurationSection
{
   [ConfigurationProperty("Services", IsDefaultCollection = false)]
   [ConfigurationCollection(typeof(ServiceCollection),
       AddItemName = "add",
       ClearItemsName = "clear",
       RemoveItemName = "remove")]
   public ServiceCollection Services
   {
      get
      {
         return (ServiceCollection)base["Services"];
      }
   }
}

And that should do the trick. To consume it you can use:

ServiceConfigurationSection serviceConfigSection =
   ConfigurationManager.GetSection("ServicesSection") as ServiceConfigurationSection;

ServiceConfig serviceConfig = serviceConfigSection.Services[0];

Solution 2

If you are looking for a custom configuration section like following

<CustomApplicationConfig>
        <Credentials Username="itsme" Password="mypassword"/>
        <PrimaryAgent Address="10.5.64.26" Port="3560"/>
        <SecondaryAgent Address="10.5.64.7" Port="3570"/>
        <Site Id="123" />
        <Lanes>
          <Lane Id="1" PointId="north" Direction="Entry"/>
          <Lane Id="2" PointId="south" Direction="Exit"/>
        </Lanes> 
</CustomApplicationConfig>

then you can use my implementation of configuration section so to get started add System.Configuration assembly reference to your project

Look at the each nested elements I used, First one is Credentials with two attributes so lets add it first

Credentials Element

public class CredentialsConfigElement : System.Configuration.ConfigurationElement
    {
        [ConfigurationProperty("Username")]
        public string Username
        {
            get 
            {
                return base["Username"] as string;
            }
        }

        [ConfigurationProperty("Password")]
        public string Password
        {
            get
            {
                return base["Password"] as string;
            }
        }
    }

PrimaryAgent and SecondaryAgent

Both has the same attributes and seem like a Address to a set of servers for a primary and a failover, so you just need to create one element class for both of those like following

public class ServerInfoConfigElement : ConfigurationElement
    {
        [ConfigurationProperty("Address")]
        public string Address
        {
            get
            {
                return base["Address"] as string;
            }
        }

        [ConfigurationProperty("Port")]
        public int? Port
        {
            get
            {
                return base["Port"] as int?;
            }
        }
    }

I'll explain how to use two different element with one class later in this post, let us skip the SiteId as there is no difference in it. You just have to create one class same as above with one property only. let us see how to implement Lanes collection

it is splitted in two parts first you have to create an element implementation class then you have to create collection element class

LaneConfigElement

public class LaneConfigElement : ConfigurationElement
    {
        [ConfigurationProperty("Id")]
        public string Id
        {
            get
            {
                return base["Id"] as string;
            }
        }

        [ConfigurationProperty("PointId")]
        public string PointId
        {
            get
            {
                return base["PointId"] as string;
            }
        }

        [ConfigurationProperty("Direction")]
        public Direction? Direction
        {
            get
            {
                return base["Direction"] as Direction?;
            }
        }
    }

    public enum Direction
    { 
        Entry,
        Exit
    }

you can notice that one attribute of LanElement is an Enumeration and if you try to use any other value in configuration which is not defined in Enumeration application will throw an System.Configuration.ConfigurationErrorsException on startup. Ok lets move on to Collection Definition

[ConfigurationCollection(typeof(LaneConfigElement), AddItemName = "Lane", CollectionType = ConfigurationElementCollectionType.BasicMap)]
    public class LaneConfigCollection : ConfigurationElementCollection
    {
        public LaneConfigElement this[int index]
        {
            get { return (LaneConfigElement)BaseGet(index); }
            set
            {
                if (BaseGet(index) != null)
                {
                    BaseRemoveAt(index);
                }
                BaseAdd(index, value);
            }
        }

        public void Add(LaneConfigElement serviceConfig)
        {
            BaseAdd(serviceConfig);
        }

        public void Clear()
        {
            BaseClear();
        }

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

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

        public void Remove(LaneConfigElement serviceConfig)
        {
            BaseRemove(serviceConfig.Id);
        }

        public void RemoveAt(int index)
        {
            BaseRemoveAt(index);
        }

        public void Remove(String name)
        {
            BaseRemove(name);
        }

    }

you can notice that I have set the AddItemName = "Lane" you can choose whatever you like for your collection entry item, i prefer to use "add" the default one but i changed it just for the sake of this post.

Now all of our nested Elements have been implemented now we should aggregate all of those in a class which has to implement System.Configuration.ConfigurationSection

CustomApplicationConfigSection

public class CustomApplicationConfigSection : System.Configuration.ConfigurationSection
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(CustomApplicationConfigSection));
        public const string SECTION_NAME = "CustomApplicationConfig";

        [ConfigurationProperty("Credentials")]
        public CredentialsConfigElement Credentials
        {
            get
            {
                return base["Credentials"] as CredentialsConfigElement;
            }
        }

        [ConfigurationProperty("PrimaryAgent")]
        public ServerInfoConfigElement PrimaryAgent
        {
            get
            {
                return base["PrimaryAgent"] as ServerInfoConfigElement;
            }
        }

        [ConfigurationProperty("SecondaryAgent")]
        public ServerInfoConfigElement SecondaryAgent
        {
            get
            {
                return base["SecondaryAgent"] as ServerInfoConfigElement;
            }
        }

        [ConfigurationProperty("Site")]
        public SiteConfigElement Site
        {
            get
            {
                return base["Site"] as SiteConfigElement;
            }
        }

        [ConfigurationProperty("Lanes")]
        public LaneConfigCollection Lanes
        {
            get { return base["Lanes"] as LaneConfigCollection; }
        }
    }

Now you can see that we have two properties with name PrimaryAgent and SecondaryAgent both have the same type now you can easily understand why we had only one implementation class against these two element.

Before you can use this newly invented configuration section in your app.config (or web.config) you just need to tell you application that you have invented your own configuration section and give it some respect, to do so you have to add following lines in app.config (may be right after start of root tag).

<configSections>
    <section name="CustomApplicationConfig" type="MyNameSpace.CustomApplicationConfigSection, MyAssemblyName" />
  </configSections>

NOTE: MyAssemblyName should be without .dll e.g. if you assembly file name is myDll.dll then use myDll instead of myDll.dll

to retrieve this configuration use following line of code any where in your application

CustomApplicationConfigSection config = System.Configuration.ConfigurationManager.GetSection(CustomApplicationConfigSection.SECTION_NAME) as CustomApplicationConfigSection;

I hope above post would help you to get started with a bit complicated kind of custom config sections.

Happy Coding :)

****Edit**** To Enable LINQ on LaneConfigCollection you have to implement IEnumerable<LaneConfigElement>

And Add following implementation of GetEnumerator

public new IEnumerator<LaneConfigElement> GetEnumerator()
        {
            int count = base.Count;
            for (int i = 0; i < count; i++)
            {
                yield return base.BaseGet(i) as LaneConfigElement;
            }
        }

for the people who are still confused about how yield really works read this nice article

Two key points taken from above article are

it doesn’t really end the method’s execution. yield return pauses the method execution and the next time you call it (for the next enumeration value), the method will continue to execute from the last yield return call. It sounds a bit confusing I think… (Shay Friedman)

Yield is not a feature of the .Net runtime. It is just a C# language feature which gets compiled into simple IL code by the C# compiler. (Lars Corneliussen)

Solution 3

This is generic code for configuration collection :

public class GenericConfigurationElementCollection<T> :   ConfigurationElementCollection, IEnumerable<T> where T : ConfigurationElement, new()
{
    List<T> _elements = new List<T>();

    protected override ConfigurationElement CreateNewElement()
    {
        T newElement = new T();
        _elements.Add(newElement);
        return newElement;
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return _elements.Find(e => e.Equals(element));
    }

    public new IEnumerator<T> GetEnumerator()
    {
        return _elements.GetEnumerator();
    }
}

After you have GenericConfigurationElementCollection, you can simple use it in the config section (this is an example from my Dispatcher):

public class  DispatcherConfigurationSection: ConfigurationSection
{
    [ConfigurationProperty("maxRetry", IsRequired = false, DefaultValue = 5)]
    public int MaxRetry
    {
        get
        {
            return (int)this["maxRetry"];
        }
        set
        {
            this["maxRetry"] = value;
        }
    }

    [ConfigurationProperty("eventsDispatches", IsRequired = true)]
    [ConfigurationCollection(typeof(EventsDispatchConfigurationElement), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
    public GenericConfigurationElementCollection<EventsDispatchConfigurationElement> EventsDispatches
    {
        get { return (GenericConfigurationElementCollection<EventsDispatchConfigurationElement>)this["eventsDispatches"]; }
    }
}

The Config Element is config Here:

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

The config file would look like this:

<?xml version="1.0" encoding="utf-8" ?>
  <dispatcherConfigurationSection>
    <eventsDispatches>
      <add name="Log" ></add>
      <add name="Notification" ></add>
      <add name="tester" ></add>
    </eventsDispatches>
  </dispatcherConfigurationSection>

Hope it help !

Solution 4

An easier alternative for those who would prefer not to write all that configuration boilerplate manually...

1) Install Nerdle.AutoConfig from NuGet

2) Define your ServiceConfig type (either a concrete class or just an interface, either will do)

public interface IServiceConfiguration
{
    int Port { get; }
    ReportType ReportType { get; }
}

3) You'll need a type to hold the collection, e.g.

public interface IServiceCollectionConfiguration
{
    IEnumerable<IServiceConfiguration> Services { get; } 
}

4) Add the config section like so (note camelCase naming)

<configSections>
  <section name="serviceCollection" type="Nerdle.AutoConfig.Section, Nerdle.AutoConfig"/>
</configSections>

<serviceCollection>
  <services>
    <service port="6996" reportType="File" />
    <service port="7001" reportType="Other" />
  </services>
</serviceCollection>

5) Map with AutoConfig

var services = AutoConfig.Map<IServiceCollectionConfiguration>();

Solution 5

Try inheriting from ConfigurationSection. This blog post by Phil Haack has an example.

Confirmed, per the documentation for IConfigurationSectionHandler:

In .NET Framework version 2.0 and above, you must instead derive from the ConfigurationSection class to implement the related configuration section handler.

Share:
162,683

Related videos on Youtube

Chris Holmes
Author by

Chris Holmes

Updated on August 28, 2020

Comments

  • Chris Holmes
    Chris Holmes over 3 years

    I am trying to implement a custom configuration section in a project and I keep running up against exceptions that I do not understand. I am hoping someone can fill in the blanks here.

    I have App.config that looks like this:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
        <configSections>
            <section name="ServicesSection" type="RT.Core.Config.ServicesConfigurationSectionHandler, RT.Core"/>
        </configSections>
        <ServicesSection type="RT.Core.Config.ServicesSection, RT.Core">
                <Services>
                    <AddService Port="6996" ReportType="File" />
                    <AddService Port="7001" ReportType="Other" />
                </Services>
            </ServicesSection>
    </configuration>
    

    I have a ServiceConfig element defined like so:

    public class ServiceConfig : ConfigurationElement
      {
        public ServiceConfig() {}
    
        public ServiceConfig(int port, string reportType)
        {
          Port = port;
          ReportType = reportType;
        }
    
        [ConfigurationProperty("Port", DefaultValue = 0, IsRequired = true, IsKey = true)]
        public int Port 
        {
          get { return (int) this["Port"]; }
          set { this["Port"] = value; }
        }
    
        [ConfigurationProperty("ReportType", DefaultValue = "File", IsRequired = true, IsKey = false)]
        public string ReportType
        {
          get { return (string) this["ReportType"]; }
          set { this["ReportType"] = value; }
        }
      }
    

    And I have a ServiceCollection defined like so:

    public class ServiceCollection : ConfigurationElementCollection
      {
        public ServiceCollection()
        {
          Console.WriteLine("ServiceCollection Constructor");
        }
    
        public ServiceConfig this[int index]
        {
          get { return (ServiceConfig)BaseGet(index); }
          set
          {
            if (BaseGet(index) != null)
            {
              BaseRemoveAt(index);
            }
            BaseAdd(index, value);
          }
        }
    
        public void Add(ServiceConfig serviceConfig)
        {
          BaseAdd(serviceConfig);
        }
    
        public void Clear()
        {
          BaseClear();
        }
    
        protected override ConfigurationElement CreateNewElement()
        {
          return new ServiceConfig();
        }
    
        protected override object GetElementKey(ConfigurationElement element)
        {
          return ((ServiceConfig) element).Port;
        }
    
        public void Remove(ServiceConfig serviceConfig)
        {
          BaseRemove(serviceConfig.Port);
        }
    
        public void RemoveAt(int index)
        {
          BaseRemoveAt(index);
        }
    
        public void Remove(string name)
        {
          BaseRemove(name);
        }
      }
    

    The part I am missing is what to do for the handler. Originally, I tried to implement an IConfigurationSectionHandler but found two things:

    1. it didn't work
    2. it's deprecated.

    I'm completely lost now on what to do so I can read my data from config. Any help please!

    • sirdank
      sirdank almost 9 years
      I can't get this working. I'd love to see RT.Core.Config.ServicesSection. I just get Unrecognized element 'AddService' despite using the code from the accepted answer as well.
    • HeatherD
      HeatherD about 7 years
      I missed this at first too - this part: [ConfigurationCollection(typeof(ServiceCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")] The AddItemName has to match so if you changed "add" to "addService" it would work
  • abatishchev
    abatishchev over 13 years
    Cool! Was thinking about the same and found that I'm not alone. Wish MS implement that for all FCL configs
  • Wim Coenen
    Wim Coenen about 12 years
    The [Add|Remove|Clear]ItemName properties on the ConfigurationCollection attribute aren't really necessary in this case, because "add"/"clear"/"remove" are already the default names of the XML elements.
  • JonathanWolfson
    JonathanWolfson over 11 years
    How can I make it work so that the tags are not adds? It only seems to work if they are add. It wouldn't work if it was <Service Port="6996" ReportType="File" /> or <Service Port="7001" ReportType="Other" />
  • SpaceCowboy74
    SpaceCowboy74 about 11 years
    Any suggestion on how to do that with a BasicMap for the Items? I don't want to implement Add if i can avoid it.
  • Mubashar
    Mubashar over 10 years
    @JonathanWolfson: just change AddItemName = "add" to AddItemName = "Service"
  • crush
    crush almost 10 years
    Is this still the approach for .NET 4.5?
  • Russell McClure
    Russell McClure almost 10 years
    @crush: yes, not much changes in this dusty corner of .NET.
  • John Leidegren
    John Leidegren about 7 years
    Thanks for providing a complete example, this really helps a lot!
  • Svend
    Svend over 6 years
    Thank God for this answer
  • CodeThief
    CodeThief over 4 years
    For people who just want to get it done and not necessarily create everything from scratch, this is the real answer :)
  • Gh61
    Gh61 over 3 years
    Answers starting with "install {something}" are not very good answers
  • Eric Hirst
    Eric Hirst over 3 years
    @Gh61 nothing in stackoverflow.com/help/how-to-answer supports that opinion. After seeing this answer and trying Nerdle.Autoconfig, I'll personally never write ConfigurationSection code again.