Sunday 10 July 2011

Nested Custom Configuration Collections in C#

I've working in a little project to automate the creation of SSH tunnels, so that we don't have to launch a Putty session with all the right tunnels. Lazy? perhaps, but there is nothing like laziness to spur novel solutions, not that this is one of these situations.
At any rate, back to the problem. With a multitude of tunnels and potentially SSH servers on the other side of the NAT divide, I wanted a configuration section like this:

   1 <TunnelSection>
   2     <host SSHServerHostname="tsg" username="user" SSHport="22" password="" privatekey="" privatekeypassphrase="">
   3       <tunnels>
   4         <tunnel name="tfs" localport="8081"  remoteport="8080" destinationserver="tfs.dev.com"  />
   5         <tunnel name="sql" localport="14331"  remoteport="1433" destinationserver="sql2008.dev.com"  />
   6         <tunnel name="crmapp" localport="81"  remoteport="80" destinationserver="crmapp.dev.com"  />
   7       </tunnels>
   8     </host>
   9     <host SSHServerHostname="blade16" username="root" SSHport="22"  password="" privatekey="" privatekeypassphrase="">
  10     <tunnels>
  11       <tunnel name="vnc" localport="5902"  remoteport="5902" destinationserver="blade1.dev.com" />
  12     </tunnels>
  13     </host>
  14 </TunnelSection>

The example in the microsoft website does not quite do what I wanted to do, although, I suspect that I might have been able to twist it to get it working. At any rate, here is the code:

   1 using System;
   2 using System.Configuration;
   3 
   4 
   5 namespace SSHTunnelWF
   6 {
   7 
   8 
   9   public class TunnelSection : ConfigurationSection
  10     {
  11 
  12         [ConfigurationProperty("", IsDefaultCollection = true)]  
  13         public HostCollection Tunnels
  14         {
  15             get
  16             {
  17                 HostCollection hostCollection = (HostCollection)base[""];
  18 
  19                 return hostCollection;                
  20 
  21             }
  22         }
  23 
  24     }
  25 
  26 
  27     public class HostCollection : ConfigurationElementCollection
  28     {
  29         public HostCollection()
  30         {
  31             HostConfigElement details = (HostConfigElement)CreateNewElement();
  32             if (details.SSHServerHostname != "")
  33             {
  34                 Add(details);
  35             }
  36         }
  37 
  38         public override ConfigurationElementCollectionType CollectionType
  39         {
  40             get
  41             {
  42                 return ConfigurationElementCollectionType.BasicMap;
  43             }
  44         }
  45 
  46         protected override ConfigurationElement CreateNewElement()
  47         {
  48             return new HostConfigElement();
  49         }
  50 
  51         protected override Object GetElementKey(ConfigurationElement element)
  52         {
  53             return ((HostConfigElement)element).SSHServerHostname;
  54         }
  55 
  56         public HostConfigElement this[int index]
  57         {
  58             get
  59             {
  60                 return (HostConfigElement)BaseGet(index);
  61             }
  62             set
  63             {
  64                 if (BaseGet(index) != null)
  65                 {
  66                     BaseRemoveAt(index);
  67                 }
  68                 BaseAdd(index, value);
  69             }
  70         }
  71 
  72         new public HostConfigElement this[string name]
  73         {
  74             get
  75             {
  76                 return (HostConfigElement)BaseGet(name);
  77             }
  78         }
  79 
  80         public int IndexOf(HostConfigElement details)
  81         {
  82             return BaseIndexOf(details);
  83         }
  84 
  85         public void Add(HostConfigElement details)
  86         {
  87             BaseAdd(details);
  88         }
  89         protected override void BaseAdd(ConfigurationElement element)
  90         {
  91             BaseAdd(element, false);
  92         }
  93 
  94         public void Remove(HostConfigElement details)
  95         {
  96             if (BaseIndexOf(details) >= 0)
  97                 BaseRemove(details.SSHServerHostname);
  98         }
  99 
 100         public void RemoveAt(int index)
 101         {
 102             BaseRemoveAt(index);
 103         }
 104 
 105         public void Remove(string name)
 106         {
 107             BaseRemove(name);
 108         }
 109 
 110         public void Clear()
 111         {
 112             BaseClear();
 113         }
 114 
 115         protected override string ElementName
 116         {
 117             get { return "host"; }
 118         }
 119     }
 120 
 121 
 122     public class HostConfigElement:ConfigurationElement
 123     {
 124 
 125         [ConfigurationProperty("SSHServerHostname", IsRequired = true, IsKey = true)]
 126         [StringValidator(InvalidCharacters = "  ~!@#$%^&*()[]{}/;’\"|\\")]
 127         public string SSHServerHostname
 128         {
 129             get { return (string)this["SSHServerHostname"]; }
 130             set { this["SSHServerHostname"] = value; }
 131         }
 132 
 133         [ConfigurationProperty("username", IsRequired = true)]
 134         [StringValidator(InvalidCharacters = "  ~!@#$%^&*()[]{}/;’\"|\\")]
 135         public string Username
 136         {
 137             get { return (string)this["username"]; }
 138             set { this["username"] = value; }
 139         }
 140         [ConfigurationProperty("SSHport", IsRequired = true, DefaultValue = 22)]
 141         [IntegerValidator(MinValue = 1, MaxValue = 65536)]
 142         public int SSHPort
 143         {
 144             get { return (int)this["SSHport"]; }
 145             set { this["SSHport"] = value; }
 146         }
 147 
 148        [ConfigurationProperty("password", IsRequired = false)]
 149         public string Password
 150         {
 151             get { return (string)this["password"]; }
 152             set { this["password"] = value; }
 153         }
 154 
 155         [ConfigurationProperty("privatekey", IsRequired = false)]
 156         public string Privatekey
 157         {
 158             get { return (string)this["privatekey"]; }
 159             set { this["privatekey"] = value; }
 160         }
 161 
 162         [ConfigurationProperty("privatekeypassphrase", IsRequired = false)]
 163         public string Privatekeypassphrase
 164         {
 165             get { return (string)this["privatekeypassphrase"]; }
 166             set { this["privatekeypassphrase"] = value; }
 167         }
 168 
 169         [ConfigurationProperty("tunnels", IsDefaultCollection = false)]
 170         public TunnelCollection Tunnels
 171         {
 172             get { return (TunnelCollection)base["tunnels"]; }
 173             
 174         }
 175 
 176     }
 177 
 178 
 179     public class TunnelCollection : ConfigurationElementCollection
 180     {
 181 
 182         public new TunnelConfigElement this[string name]
 183         {
 184             get
 185             {
 186                 if (IndexOf(name) < 0) return null;
 187 
 188                 return (TunnelConfigElement)BaseGet(name);
 189             }
 190         }
 191 
 192         public TunnelConfigElement this[int index]
 193         {
 194             get { return (TunnelConfigElement)BaseGet(index); }
 195         }
 196 
 197         public int IndexOf(string name)
 198         {
 199             name = name.ToLower();
 200 
 201             for (int idx = 0; idx < base.Count; idx++)
 202             {
 203                 if (this[idx].Name.ToLower() == name)
 204                     return idx;
 205             }
 206             return -1;
 207         }
 208 
 209         public override ConfigurationElementCollectionType CollectionType
 210         {
 211             get { return ConfigurationElementCollectionType.BasicMap; }
 212         }
 213 
 214         protected override ConfigurationElement CreateNewElement()
 215         {
 216             return new TunnelConfigElement();
 217         }
 218 
 219         protected override object GetElementKey(ConfigurationElement element)
 220         {
 221             return ((TunnelConfigElement)element).Name;
 222         }
 223 
 224         protected override string ElementName
 225         {
 226             get { return "tunnel"; }
 227         }
 228 
 229     }
 230 
 231 
 232     public class TunnelConfigElement : ConfigurationElement
 233     {        
 234 
 235         public TunnelConfigElement()
 236     {
 237     }
 238 
 239         public TunnelConfigElement(string name, int localport, int remoteport, string destinationserver)
 240         {
 241            
 242             this.DestinationServer = destinationserver;
 243             this.RemotePort = remoteport;
 244             this.LocalPort = localport;            
 245             this.Name = name;
 246         }
 247 
 248         [ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")]       
 249         public string Name
 250         {
 251             get { return (string)this["name"]; }
 252             set { this["name"] = value; }
 253         }        
 254 
 255         [ConfigurationProperty("localport", IsRequired = true, DefaultValue =1)]
 256         [IntegerValidator(MinValue = 1, MaxValue = 65536)]
 257         public int LocalPort
 258         {
 259             get { return (int)this["localport"]; }
 260             set { this["localport"] = value; }
 261         }
 262 
 263         [ConfigurationProperty("remoteport", IsRequired = true, DefaultValue =1)]
 264         [IntegerValidator(MinValue = 1, MaxValue = 65536)]
 265         public int RemotePort
 266         {
 267             get { return (int)this["remoteport"]; }
 268             set { this["remoteport"] = value; }
 269         }
 270 
 271         [ConfigurationProperty("destinationserver", IsRequired = true)]
 272         [StringValidator(InvalidCharacters = "  ~!@#$%^&*()[]{}/;’\"|\\")]
 273         public string DestinationServer
 274         {
 275             get { return (string)this["destinationserver"]; }
 276             set { this["destinationserver"] = value; }
 277         }
 278 
 279     }
 280 
 281 }

The nesting of the collections is achieved by defining a ConfigurationProperty as a class that inherits from the ConfigurationElementCollection class, TunnelCollection in this case, as part of HostConfigElement, which contains all the configuration properties required by the host. This could be repeated ad infinitum by adding a new ConfigurationProperties to the TunnelConfigElement. Make sure that the ElementName of the collection is overridden and matches your config file, in this case host for the HostCollection and tunnel for the TunnelCollection.

Here is the configuration file. Note that the type needs to reference the ConfigSection class, SSHTunnelWF.TunnelSection in this case, and the assembly name SSHTunnelWF:

   1 <?xml version="1.0"?>
   2 <configuration>
   3   <configSections>
   4     <section name="TunnelSection" type="SSHTunnelWF.TunnelSection,SSHTunnelWF" />
   5   </configSections>
   6   <startup>
   7     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
   8   </startup>
   9   <TunnelSection>
  10     <host SSHServerHostname="tsg.edssdn.net" username="user" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
  11       <tunnels>
  12         <tunnel name="tfs" localport="8081"  remoteport="8080" destinationserver="tfs2010.dev.com"  />
  13         <tunnel name="sql" localport="14331"  remoteport="1433" destinationserver="sql2008.dev.com"  />
  14         <tunnel name="crm2011app" localport="81"  remoteport="80" destinationserver="crm2011betaapp.dev.com"  />
  15       </tunnels>
  16     </host>
  17     <host SSHServerHostname="blade16" username="root" SSHport="22"  password="pass" privatekey="" privatekeypassphrase="">
  18      <tunnels>
  19        <tunnel name="vnc" localport="5902"  remoteport="5902" destinationserver="blade1.dev.com" />
  20      </tunnels>
  21     </host>
  22   </TunnelSection>
  23 </configuration>


Finally, you can access the contents of this custom configuration section like this:

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

28 comments:

  1. THANK YOU!


    Note. I added this.....implementing IList<>.
    You'll have to add all the methods of the interface, and I provided the one implementation you must have for the LINQ code to work.


    public class HostCollection : ConfigurationElementCollection , IList
    {

    /*
    all the other code you already have
    */

    /*
    a bunch of other IList methods, but I left them as default.
    */

    public new bool IsReadOnly
    {
    get { return true; }
    }

    public new IEnumerator GetEnumerator()
    {

    /* see http://connect.microsoft.com/VisualStudio/feedback/details/713688/stackoverflowexception-when-calling-enumerable-cast-t-on-domaincollectionview-titem */
    return this.OfType().GetEnumerator();
    }

    }

    And this allowed me to have strong access to items in the collection.


    HostCollection hosts = /* some code here to get a HostCollection */;

    /* now we have the collection, let's fish for one */

    HostConfigElement founditem = hosts.FirstOrDefault(d => d.SSHServerHostname.Equals("Jer333", StringComparison.OrdinalIgnoreCase));
    if (null != founditem)
    {
    Console.Writeline("I found an item using LINQ!");
    }


    But your nested collection(s) example saved the day, thanks.

    ReplyDelete
    Replies
    1. It thinks the List tag is xml....so my code above looks a little off.

      public class HostCollection : ConfigurationElementCollection , IList (less-than sign)HostConfigElement(greater-than symbol)
      {}

      Replace (less-than sign) and (greater-than symbol) with the c# "tags" for wrapping a Generic.

      Delete
    2. Thank you @grandacoder for sharing this link in stackoverflow thread (http://stackoverflow.com/questions/5661544/correct-implementation-of-a-custom-config-section-with-nested-collections). Appreciate you and a big thank you to the author of this article. I would appreciate if you can share your IList<> implementation. I tried but not able to implement it. It would be really helpful. PLEASE!

      Delete
    3. This comment has been removed by the author.

      Delete
    4. Thanks a million!

      Delete
  2. Nice work. Thanks for the page.

    ReplyDelete
  3. Hi,
    This is Sumit Sood. Thank you very much for this useful post. You saved a lot of blood. Refered many blogs but all invain, got best solution here.

    ReplyDelete
  4. But can you Delete a single tunnel element in the tunnels collection? if so HOW?

    ReplyDelete
  5. AMAZING!!! Worked like a charm! THANK YOU!!!!

    ReplyDelete
  6. AMAZING!!!!!!!!! HELPED ME A LOT!

    ReplyDelete
  7. Cool. Thanks as it helped me out.

    ReplyDelete
  8. This is awesome. Thank you very much for this. It helps me save quite a bit of time.

    ReplyDelete
  9. Thank you! This is one of the few examples that have a collection of configuration elements (Host) directly under the root element (TunnelSection). All the other examples have an extra level of crap that make it look like TunnelSection > Tunnels > Host.

    ReplyDelete
  10. Found you on stackoverflow, great article! Why is this kind of information never on MSDN? :)

    http://stackoverflow.com/questions/5661544/correct-implementation-of-a-custom-config-section-with-nested-collections

    ReplyDelete
  11. Excellent thanks!

    ReplyDelete
  12. How do I loop over Tunnels after this:

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

    ReplyDelete
  13. Fantastic post, even in 2017. Saved me a bunch of time.

    ReplyDelete
  14. Thank you. How would I change it to avoid the tag?

    ReplyDelete
  15. Please, could anyone help me? I've posted a question similar to this on Stack Overflow.

    In my case, I have configuration group settings as well.

    Link:
    https://stackoverflow.com/questions/44678901/c-sharp-custom-configuration-group-settings

    ReplyDelete
  16. great perfect thks

    ReplyDelete
  17. Your style is really unique in comparison to other people I have read stuff from. Thank you for posting when you've got the opportunity, Guess I will just book mark this blog.

    ReplyDelete
  18. I delight in, cause I discovered exactly what I was taking a look for. You have ended my 4 day lengthy hunt! God Bless you man. Have a nice day. Bye

    ReplyDelete
  19. Thank you very much for this great example.

    ReplyDelete