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
THANK YOU!
ReplyDeleteNote. 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.
It thinks the List tag is xml....so my code above looks a little off.
Deletepublic 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.
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!
DeleteThis comment has been removed by the author.
DeleteThanks a million!
DeleteNice work. Thanks for the page.
ReplyDeleteHi,
ReplyDeleteThis 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.
But can you Delete a single tunnel element in the tunnels collection? if so HOW?
ReplyDeleteAMAZING!!! Worked like a charm! THANK YOU!!!!
ReplyDeleteAMAZING!!!!!!!!! HELPED ME A LOT!
ReplyDeleteCool. Thanks as it helped me out.
ReplyDeleteThis is awesome. Thank you very much for this. It helps me save quite a bit of time.
ReplyDeleteThank 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.
ReplyDeleteFound you on stackoverflow, great article! Why is this kind of information never on MSDN? :)
ReplyDeletehttp://stackoverflow.com/questions/5661544/correct-implementation-of-a-custom-config-section-with-nested-collections
Excellent thanks!
ReplyDeleteHow do I loop over Tunnels after this:
ReplyDeleteTunnelSection tunnels = ConfigurationManager.GetSection("TunnelSection") as TunnelSection
Fantastic post, even in 2017. Saved me a bunch of time.
ReplyDeleteThank you. How would I change it to avoid the tag?
ReplyDeletePlease, could anyone help me? I've posted a question similar to this on Stack Overflow.
ReplyDeleteIn my case, I have configuration group settings as well.
Link:
https://stackoverflow.com/questions/44678901/c-sharp-custom-configuration-group-settings
great perfect thks
ReplyDeleteThis worked great! Thanks!
ReplyDeleteThanks, it was really helpful!
ReplyDeleteWorks perfectly. Thank you!!
ReplyDeleteTHANK YOU!!
ReplyDeleteYour 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.
ReplyDeleteI 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
ReplyDeleteThank you very much for this great example.
ReplyDeleteyozgat
ReplyDeleteadana
adıyaman
afyon
aksaray
Ä°3R5T