How to Implement a Configurationsection with a Configurationelementcollection

How to implement a ConfigurationSection with a ConfigurationElementCollection

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];

Use ConfigurationSection / ConfigurationElementCollection to create a nested collection of items from web.config

To fix your immediate error, you just need to add wrapping <email> tag to your <add/> elements. That is because your SystemEmailsElement contains Emails property which is collection with root tag defined as "email" (via [ConfigurationProperty("email")]). Like this:

<systemEmails>
<emails>
<email name="SystemEmail01" defaultAddress="SystemEmail01@email.com" defaultName="John Doe 01" />
<email name="SystemEmail02" source="UserName" username="Username02" defaultAddress="SystemEmail02@email.com" defaultName="Jane Doe 02" />
<email name="SystemEmail03" defaultAddress="SystemEmail03@email.com" defaultName="John Doe 03" />
<email name="SystemEmail04" source="UserName" username="Username04" />
<email name="SystemEmail05" source="RoleName" rolename="administrators" defaultEmailName="SystemEmail02" />
<email name="SystemEmailGroup01" source="Group">
<email>
<add name="SystemEmail06" defaultAddress="SystemEmail06@email.com" defaultName="Jane Doe 06" />
<!-- ^^^ LINE 736 ^^^ -->
<add name="SystemEmail07" defaultAddress="SystemEmail07@email.com" defaultName="John Doe 07" />
<add name="SystemEmail08" defaultAddress="SystemEmail08@email.com" defaultName="Jane Doe 08" />
</email>
</email>
</emails>
</systemEmails>

Now, to make it work like in ideal world, you can utilize IsDefaultCollection property of ConfigurationProperty attribute like this:

[ConfigurationProperty("", IsDefaultCollection = true)]
[ConfigurationCollection(typeof (SystemEmailsElementCollection),
AddItemName = "email")]
public SystemEmailsElementCollection Emails
{
get { return base[""] as SystemEmailsElementCollection; }
}

This will allow you to create recursive email nodes the way you would expect to:

  <emails>
<email name="SystemEmail01" defaultAddress="SystemEmail01@email.com" defaultName="John Doe 01" />
<email name="SystemEmail02" source="UserName" username="Username02" defaultAddress="SystemEmail02@email.com" defaultName="Jane Doe 02" />
<email name="SystemEmail03" defaultAddress="SystemEmail03@email.com" defaultName="John Doe 03" />
<email name="SystemEmail04" source="UserName" username="Username04" />
<email name="SystemEmail05" source="RoleName" rolename="administrators" defaultEmailName="SystemEmail02" />
<email name="SystemEmailGroup01" source="Group">
<email name="SystemEmail06" defaultAddress="SystemEmail06@email.com" defaultName="Jane Doe 06">
<email name="SystemEmail04" source="UserName" username="Username04" />
<email name="SystemEmail05" source="RoleName" rolename="administrators" defaultEmailName="SystemEmail02" />
</email>
<!-- ^^^ LINE 736 ^^^ -->
<email name="SystemEmail07" defaultAddress="SystemEmail07@email.com" defaultName="John Doe 07" />
<email name="SystemEmail08" defaultAddress="SystemEmail08@email.com" defaultName="Jane Doe 08" />
</email>
</emails>

How to Create a Configuration Section That Contains a Collection

You don't need to inherit from both ConfigurationSection and ConfigurationElementCollection. Instead, define your config section like this:

public class CustomConfigSection : ConfigurationSection
{
[ConfigurationProperty("", IsDefaultCollection = true)]
public MyConfigElementCollection ConfigElementCollection
{
get
{
return (MyConfigElementCollection)base[""];
}
}
}

And your config element collection:

[ConfigurationCollection(typeof(MyConfigElement), AddItemName = "ConfigElement"]
public class MyConfigElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new MyConfigElement();
}

protected override object GetElementKey(ConfigurationElement element)
{
if (element == null)
throw new ArgumentNullException("element");

return ((MyConfigElement)element).key;
}
}

And config element itself:

public class MyConfigElement: ConfigurationElement
{
[ConfigurationProperty("key", IsRequired = true, IsKey = true)]
public string Key
{
get
{
return (string)base["key"];
}
}
}

Implement custom section with a Configuration Element Collection c#

The problems appear to be in your app.config (or web.config). The element that contains your custom configuration XML must match the name you have specified in the name attribute in configSections\section. For example, for your code to work as written, the app.config should look something like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="s" type="Statistics.Config.DepartmentConfigurationSection, Program1"/>
</configSections>
<s>
<Cash>
<add Number="1" Name="Money" />
</Cash>
<Departments>
<add Id="1" Name="x" />
<add Id="2" Name="y" />
</Departments>
</s>
</configuration>

As you can see, the section name="s" matches the name of the s element. Also, you had the type listed as Statistics.Config.DeptartmentSection, but your class name is DepartmentConfigurationSection, so it should match the class you are trying to load.

ConfigurationElementCollection with primitive types

I was able to get this to work without much customization. It is similar to JerKimball's answer but I avoids processing the custom string processing by using a TypeConverter attribute for the ConfigurationProperty.

My custom config section implementation:

using System.Configuration;
using System.ComponentModel;

class DomainConfig : ConfigurationSection
{

[ConfigurationProperty("DoubleArray")]
[TypeConverter(typeof(CommaDelimitedStringCollectionConverter))]
public CommaDelimitedStringCollection DoubleArray
{
get { return (CommaDelimitedStringCollection)base["DoubleArray"]; }
}
}

How it's used:

var doubleValues = from string item in configSection.DoubleArray select double.Parse(item);

And the config file:

<DomainConfig DoubleArray="1.0,2.0,3.0"></DomainConfig>

How do I add custom ConfigurationSection to Assembly?

If I understand correctly, you have problem with resolving what actually your Assembly is, since you are only creating .cs files that determine types that this file hold.

Assembly (in maybe not so accurate shorcut) is just the project you have in your solution. It will get compiled into its seperate assembly - the .dll you mentioned - later on.
When you add class to any .cs file in given project, on compile it will be included in project's assembly.

By default, if you won't provide assembly for configSection where its corresponding type should be found, App.config defaults to System.Configuration assembly - that's where you get your error from, since you've declared your section in your own assembly (== project).

Right click in Visual Studio on your project that holds App.config file and choose Properties to check its Assembly name:

Sample Image

Then add this name to your App.config section declaration. In my example its ConsoleApp1, so I will add it to configuration accordingly:

<configSections>
<section name="testSection" type="mssql_gui.TestConfigSection, ConsoleApp1"/>
</configSections>

Correct implementation of a custom config section with nested collections?

I finally found this guy's example. It was coded and worked right out of the box.

http://manyrootsofallevilrants.blogspot.com/2011/07/nested-custom-configuration-collections.html

I am going to paste the code here......only because I cannot stand it when someone says "Your answer is here", and the link is dead.

Please try his website first, and leave a "thank you" if it works.

using System;
using System.Configuration;

namespace SSHTunnelWF
{
public class TunnelSection : ConfigurationSection
{
[ConfigurationProperty("", IsDefaultCollection = true)]
public HostCollection Tunnels
{
get
{
HostCollection hostCollection = (HostCollection)base[""];
return hostCollection;
}
}
}

public class HostCollection : ConfigurationElementCollection
{
public HostCollection()
{
HostConfigElement details = (HostConfigElement)CreateNewElement();
if (details.SSHServerHostname != "")
{
Add(details);
}
}

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

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

protected override Object GetElementKey(ConfigurationElement element)
{
return ((HostConfigElement)element).SSHServerHostname;
}

public HostConfigElement this[int index]
{
get
{
return (HostConfigElement)BaseGet(index);
}
set
{
if (BaseGet(index) != null)
{
BaseRemoveAt(index);
}
BaseAdd(index, value);
}
}

new public HostConfigElement this[string name]
{
get
{
return (HostConfigElement)BaseGet(name);
}
}

public int IndexOf(HostConfigElement details)
{
return BaseIndexOf(details);
}

public void Add(HostConfigElement details)
{
BaseAdd(details);
}

protected override void BaseAdd(ConfigurationElement element)
{
BaseAdd(element, false);
}

public void Remove(HostConfigElement details)
{
if (BaseIndexOf(details) >= 0)
BaseRemove(details.SSHServerHostname);
}

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

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

public void Clear()
{
BaseClear();
}

protected override string ElementName
{
get { return "host"; }
}
}

public class HostConfigElement:ConfigurationElement
{
[ConfigurationProperty("SSHServerHostname", IsRequired = true, IsKey = true)]
[StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
public string SSHServerHostname
{
get { return (string)this["SSHServerHostname"]; }
set { this["SSHServerHostname"] = value; }
}

[ConfigurationProperty("username", IsRequired = true)]
[StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
public string Username
{
get { return (string)this["username"]; }
set { this["username"] = value; }
}

[ConfigurationProperty("SSHport", IsRequired = true, DefaultValue = 22)]
[IntegerValidator(MinValue = 1, MaxValue = 65536)]
public int SSHPort
{
get { return (int)this["SSHport"]; }
set { this["SSHport"] = value; }
}

[ConfigurationProperty("password", IsRequired = false)]
public string Password
{
get { return (string)this["password"]; }
set { this["password"] = value; }
}

[ConfigurationProperty("privatekey", IsRequired = false)]
public string Privatekey
{
get { return (string)this["privatekey"]; }
set { this["privatekey"] = value; }
}

[ConfigurationProperty("privatekeypassphrase", IsRequired = false)]
public string Privatekeypassphrase
{
get { return (string)this["privatekeypassphrase"]; }
set { this["privatekeypassphrase"] = value; }
}

[ConfigurationProperty("tunnels", IsDefaultCollection = false)]
public TunnelCollection Tunnels
{
get { return (TunnelCollection)base["tunnels"]; }
}
}

public class TunnelCollection : ConfigurationElementCollection
{
public new TunnelConfigElement this[string name]
{
get
{
if (IndexOf(name) < 0) return null;
return (TunnelConfigElement)BaseGet(name);
}
}

public TunnelConfigElement this[int index]
{
get { return (TunnelConfigElement)BaseGet(index); }
}

public int IndexOf(string name)
{
name = name.ToLower();

for (int idx = 0; idx < base.Count; idx++)
{
if (this[idx].Name.ToLower() == name)
return idx;
}
return -1;
}

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

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

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

protected override string ElementName
{
get { return "tunnel"; }
}
}

public class TunnelConfigElement : ConfigurationElement
{
public TunnelConfigElement()
{
}

public TunnelConfigElement(string name, int localport, int remoteport, string destinationserver)
{
this.DestinationServer = destinationserver;
this.RemotePort = remoteport;
this.LocalPort = localport;
this.Name = name;
}

[ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")]
public string Name
{
get { return (string)this["name"]; }
set { this["name"] = value; }
}

[ConfigurationProperty("localport", IsRequired = true, DefaultValue =1)]
[IntegerValidator(MinValue = 1, MaxValue = 65536)]
public int LocalPort
{
get { return (int)this["localport"]; }
set { this["localport"] = value; }
}

[ConfigurationProperty("remoteport", IsRequired = true, DefaultValue =1)]
[IntegerValidator(MinValue = 1, MaxValue = 65536)]
public int RemotePort
{
get { return (int)this["remoteport"]; }
set { this["remoteport"] = value; }
}

[ConfigurationProperty("destinationserver", IsRequired = true)]
[StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
public string DestinationServer
{
get { return (string)this["destinationserver"]; }
set { this["destinationserver"] = value; }
}
}
}

And the configuration code

 <?xml version="1.0"?>
<configuration>
<configSections>
<section name="TunnelSection" type="SSHTunnelWF.TunnelSection,SSHTunnelWF" />
</configSections>
<TunnelSection>
<host SSHServerHostname="tsg.edssdn.net" username="user" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
<tunnels>
<tunnel name="tfs" localport="8081" remoteport="8080" destinationserver="tfs2010.dev.com" />
<tunnel name="sql" localport="14331" remoteport="1433" destinationserver="sql2008.dev.com" />
<tunnel name="crm2011app" localport="81" remoteport="80" destinationserver="crm2011betaapp.dev.com" />
</tunnels>
</host>
<host SSHServerHostname="blade16" username="root" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
<tunnels>
<tunnel name="vnc" localport="5902" remoteport="5902" destinationserver="blade1.dev.com" />
</tunnels>
</host>
</TunnelSection>
</configuration>

And then the "call"

TunnelSection tunnels = ConfigurationManager.GetSection("TunnelSection") as TunnelSection

Where am I going wrong with ConfigurationSections?

The secret to why this wasn't working has to do with a detail about my software that I did not share back when I wrote this question.

My application is not a stand alone application. It is a COM add-in for the VBA Editor. As such, it is a *.dll file, not an *.exe. App.config files only work with the executing assembly (the *.exe), so that is why my code was not working. There are a few good solutions here, but I ended up rolling my own configuration using XML Serialization.

Below is the code I ended up using. It can also be found in the Rubberduck repository hosted on GitHub if you prefer looking at it there.

Central to the solution is the IConfigurationService interface and the ConfigurationLoader implementation that allows me to read from and write to the xml file where the configuration is store. (The version here has been simplified to deal with the original code only.)

IConfigurationService:

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace Rubberduck.Config
{
[ComVisible(false)]
public interface IConfigurationService
{
Configuration GetDefaultConfiguration();
ToDoMarker[] GetDefaultTodoMarkers();
Configuration LoadConfiguration();
void SaveConfiguration<T>(T toSerialize);
}
}

ConfigurationLoader:

public class ConfigurationLoader : IConfigurationService
{
private static string configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Rubberduck", "rubberduck.config");

/// <summary> Saves a Configuration to Rubberduck.config XML file via Serialization.</summary>
public void SaveConfiguration<T>(T toSerialize)
{
XmlSerializer xmlSerializer = new XmlSerializer(toSerialize.GetType());
using (TextWriter textWriter = new StreamWriter(configFile))
{
xmlSerializer.Serialize(textWriter, toSerialize);
}
}

/// <summary> Loads the configuration from Rubberduck.config xml file. </summary>
/// <remarks> If an IOException occurs, returns a default configuration.</remarks>
public Configuration LoadConfiguration()
{
try
{
using (StreamReader reader = new StreamReader(configFile))
{
var deserializer = new XmlSerializer(typeof(Configuration));
var config = (Configuration)deserializer.Deserialize(reader);

//deserialization can silently fail for just parts of the config,
// so we null check and return defaults if necessary.
if (config.UserSettings.ToDoListSettings == null)
{
config.UserSettings.ToDoListSettings = new ToDoListSettings(GetDefaultTodoMarkers());
}

return config;
}
}
catch (IOException)
{
return GetDefaultConfiguration();
}
catch (InvalidOperationException ex)
{
var message = ex.Message + System.Environment.NewLine + ex.InnerException.Message + System.Environment.NewLine + System.Environment.NewLine +
configFile + System.Environment.NewLine + System.Environment.NewLine +
"Would you like to restore default configuration?" + System.Environment.NewLine +
"Warning: All customized settings will be lost.";

DialogResult result = MessageBox.Show(message, "Error Loading Rubberduck Configuration", MessageBoxButtons.YesNo,MessageBoxIcon.Exclamation);

if (result == DialogResult.Yes)
{
var config = GetDefaultConfiguration();
SaveConfiguration<Configuration>(config);
return config;
}
else
{
throw ex;
}
}
}

public Configuration GetDefaultConfiguration()
{
var userSettings = new UserSettings(new ToDoListSettings(GetDefaultTodoMarkers()));

return new Configuration(userSettings);
}

public ToDoMarker[] GetDefaultTodoMarkers()
{
var note = new ToDoMarker("NOTE:", TodoPriority.Low);
var todo = new ToDoMarker("TODO:", TodoPriority.Normal);
var bug = new ToDoMarker("BUG:", TodoPriority.High);

return new ToDoMarker[] { note, todo, bug };
}
}

Configuration:

using System;
using System.IO;
using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
[XmlRootAttribute(Namespace = "", IsNullable = false)]
public class Configuration
{
public UserSettings UserSettings { get; set; }

public Configuration()
{
//default constructor required for serialization
}

public Configuration(UserSettings userSettings)
{
this.UserSettings = userSettings;
}
}
}

User Settings:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class UserSettings
{
public ToDoListSettings ToDoListSettings { get; set; }

public UserSettings()
{
//default constructor required for serialization
}

public UserSettings(ToDoListSettings todoSettings)
{
this.ToDoListSettings = todoSettings;
}
}
}

TodoListSettings:

using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
interface IToDoListSettings
{
ToDoMarker[] ToDoMarkers { get; set; }
}

[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class ToDoListSettings : IToDoListSettings
{
[XmlArrayItemAttribute("ToDoMarker", IsNullable = false)]
public ToDoMarker[] ToDoMarkers { get; set; }

public ToDoListSettings()
{
//empty constructor needed for serialization
}

public ToDoListSettings(ToDoMarker[] markers)
{
this.ToDoMarkers = markers;
}
}
}

TodoMarkers:

using System.Xml.Serialization;
using System.Runtime.InteropServices;
using Rubberduck.VBA;

namespace Rubberduck.Config
{
[ComVisible(false)]
public enum TodoPriority
{
Low,
Normal,
High
}

[ComVisible(false)]
public interface IToDoMarker
{
TodoPriority Priority { get; set; }
string Text { get; set; }
}

[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class ToDoMarker : IToDoMarker
{
//either the code can be properly case, or the XML can be, but the xml attributes must here *exactly* match the xml
[XmlAttribute]
public string Text { get; set; }

[XmlAttribute]
public TodoPriority Priority { get; set; }

/// <summary> Default constructor is required for serialization. DO NOT USE. </summary>
public ToDoMarker()
{
// default constructor required for serialization
}

public ToDoMarker(string text, TodoPriority priority)
{
Text = text;
Priority = priority;
}

/// <summary> Convert this object into a string representation. Over-riden for easy databinding.</summary>
/// <returns> The Text property. </returns>
public override string ToString()
{
return this.Text;
}
}
}

And a sample xml file:

<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<UserSettings>
<ToDoListSettings>
<ToDoMarkers>
<ToDoMarker Text="NOTE:" Priority="Low" />
<ToDoMarker Text="TODO:" Priority="Normal" />
<ToDoMarker Text="BUG:" Priority="High" />
</ToDoMarkers>
</ToDoListSettings>
</UserSettings>
</Configuration>

How to Create a Configuration Section That Contains a Collection of Collections?

In your example config file, myConfig would be a class that inherits from ConfigurationSection with three properties named mySubConfig1, mySubConfig2 and mySubConfig3.

The type of the mySubConfig1 property (as well as 2 and 3) would be a class that inherits from ConfigurationElementCollection, implements IEnumerable<ConfigElement> and is decorated with ConfigurationCollection (where the "AddItemName" property is set to "mySubSubConfig1").

Below is a complete sample implementation of an approach I used in a production deployment. Be sure to include the System.Configuration assembly. (It's a bit confusing because the System.Configuration namespace is defined in other assmeblies, but you must include the System.Configuration assembly to use the code below.)

Here are the custom configuration classes:

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

namespace ConfigTest {
class CustomConfigSection : ConfigurationSection {

[ConfigurationProperty( "ConfigElements", IsRequired = true )]
public ConfigElementsCollection ConfigElements {
get {
return base["ConfigElements"] as ConfigElementsCollection;
}
}

}

[ConfigurationCollection( typeof( ConfigElement ), AddItemName = "ConfigElement" )]
class ConfigElementsCollection : ConfigurationElementCollection, IEnumerable<ConfigElement> {

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

protected override object GetElementKey( ConfigurationElement element ) {
var l_configElement = element as ConfigElement;
if ( l_configElement != null )
return l_configElement.Key;
else
return null;
}


Related Topics



Leave a reply



Submit