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
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>
Custom config section with two differentt child collections
You cannot have two default collections. To have configuration classes matching your xml sample, you will have to assign the names "collection1" and "collection2" respectively instead of just "" and set IsDefaultCollection to false in the ConfigurationProperty attributes, and set the AddItemName to "subitem1" and "subitem2" for the ConfigurationCollection attributes. Also fix what @thymine pointed out about the type of the second collection.
Custom section in web.config and getting all items
The full answer to my question can be found in the following SO Question:
Correct implementation of a custom config section with nested collections?
The problem is that I can't create multiple sections, but I can create multiple elements of that section via collection. With that I have to configure the classes to support my layout. Please see the SO Answer posted above for full example.
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];
Related Topics
Read Big Txt File, Out of Memory Exception
Get the Object Is Null Using JSON in Wcf Service
How to Get the Text of a Messagebox When It Has an Icon
Blocking Access to Private Member Variables? Force Use of Public Properties
How to Force Bundlecollection to Flush Cached Script Bundles in MVC4
How to Get Next (Or Previous) Enum Value in C#
MVC Validation Lower/Higher Than Other Value
What Is the Meaning of "This" in C#
ASP.NET Is There a Better Way to Find Controls That Are Within Other Controls
General Advice and Guidelines on How to Properly Override Object.Gethashcode()
Use Different Name for Serializing and Deserializing with JSON.Net
Save Detached Entity in Entity Framework 6
How to Specify Mapping Rule When Names of Properties Differ
MVC HTML.Beginform Different Url Schema
Allow Access Permission to Write in Program Files of Windows 7