![]() |
Web Development »
ASP.NET »
General
Intermediate
Creating Custom ConfigurationsBy Karl SeguinCreate isolated settings, strongly-typed objects and collections inside your web.config by leveraging the flexibility of .Net to create your own configuration sections and handler. |
C#, VB, Windows, .NET 1.0, ASP.NET, Visual Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
There have already been a number of articles written on this, so why write another one? Well, you can never have too many tutorials explaining how to do something. My goal is to present this in a cut and paste friendly manner which you'll be able to use right away, as well as provide some insight into the architecture.
I'll very quickly go over .Net's built-in configuration support since chances are you are already familiar with it. At the root of each web application sits a file called web.config. Beyond containing configuration settings for how the application behaves, the web.config defines a special section called appSetting which lets you create your own key=>value settings. A sample appSetting section might look something like this:
1: <appSettings>
2: <add key="recursiveCopy" value="true" />
3: <add key="maxDepth" value="5" />
4: <add key="sourceServer" value="10.1.1.2" />
5: <add key="sourceServerName" value="BlueBelle" />
6: <add key="server1" value="10.0.22.3" />
7: <add key="serverName1" value="CottonCandy" />
8: <add key="server2" value="10.0.22.4" />
9: <add key="serverName2" value="Butterscoth" />
10: </appSettings>This section defines a number of keys, each with a specified value. From within our code, these values can easily be retrieved via the
System.Configuration.ConfigurationSettings.AppSettings static property, for example, to retrieve the value associated with the
maxDepth key, we'd use:
1: //notice how these aren't strongly typed and have to be cast
2: int maxDepth = Convert.ToInt32(ConfigurationSettings.AppSettings["maxDepth"]);If a value is likely to change from installation to installation (such as your development environment's database connection string is almost guaranteed to be different than you client's) the placing the value where it can easily be changed is a pretty basic requirement.
There's little doubt that the above code is extremely easy to use and often times very sufficient. However, with only a few classes we can build an equally easy and far more flexible alternative. You may be asking yourself what I mean by flexible, or how the current model isn't flexible as-is. I see three major problems with the configuration model as it is:
AppSettings ConnectionString key, and your client is already using that key to point to a different database, your client has to change his or her code to accommodate yours. And if they can't, say because they are using another 3rd party component which is expecting their
ConnectionString there, you just lost a sale.ConfigurationSettings.AppSettings is just a NameValueCollection, so you can pass it anything as a value and it'll compile - but don't expect it to run. I have a serious problem with code that could be strongly typed but isn't. It puts a considerable burden on you, the developer, as opposed to the tools (intellisense and compilers) you are using.AppSetting section. I ran into this limitation while writing code to allow our network admins manually synch images to one four (or all) production image servers. My first thought was simply to add four keys, "Server1", "Server2", "Server3" and "Server4" with a network address for each one as the value. But what happens with a 5th and 6th server are added? Not only do I have to add the keys, I need to change and recompile my code. I could have done a loop in my code and checked if
"Server" + i.ToString() existed, but that's not an elegant solution.
For example, from the above web.config, to load a strongly-typed collection of Server objects, we'd need to do something like:
1: ServerCollection sc = new ServerCollection();
2: sc.Add(new Server(ConfigurationSettings.AppSettings["sourceServerName"],
ConfigurationSettings.AppSettings["sourceServer"],true));
3: sc.Add(new Server(ConfigurationSettings.AppSettings["serverName1"],
ConfigurationSettings.AppSettings["server1"],false));
4: sc.Add(new Server(ConfigurationSettings.AppSettings["serverName2"],
ConfigurationSettings.AppSettings["server2"],false));But what if we need to add another server? These values are hard-coded into the code!The goal in this tutorial is to resolve the above issues.
Just like you can add an AppSetting element to the web.config, so too can you add a configSections element. The configSection lets you define the name of your own configuration section and what handler will be used to parse it. In this part we'll use the same handler as the AppSetting, so all we'll gain is isolating our values. First off we define the new section:
1: <configuration>
2: <configSections>
3: <sectionGroup name="CompanyCo">
4: <section name="Portal"
type="System.Configuration.NameValueSectionHandler,system,
Version=1.0.3300.0,Culture=neutral,
PublicKeyToken=b77a5c561934e089,Custom=null" />
5: </sectionGroup>
6: </configSections>
7: <system.web>
8: ....
9: </system.web>
10: <CompanyCo>
11: <Portal>
12: <add key="recursiveCopy" value="true" />
13: <add key="maxDepth" value="5" />
14: <add key="sourceServer" value="10.1.1.2" />
15: <add key="sourceServerName" value="BlueBelle" />
16: <add key="server1" value="10.0.22.3" />
17: <add key="serverName1" value="CottonCandy" />
18: <add key="server2" value="10.0.22.4" />
19: <add key="serverName2" value="Butterscoth" />
20: </Portal>
21: </CompanyCo>
22: </configuration>All custom sections are defined with the <configSections> element [line: 2]. Within this section you can have 0 or more <sectionGroup> elements, which can also be nested and 0 or more <section> elements. The purpose of sectionGroups is to allow you to organize even more. As you can see, for our custom section, we tell it to use the NameValueSectionHandler [line: 4] to handle our section - this is the same as the default appSetting handler.Our custom section is defined from [line: 10-21], where we use the same <add key="xxx" value="yyy" /> syntax. Notice the relationship between our custom section's elements <CompanyCo> and <Portal> and the names we specified whey defining our sectionGroup [line: 3] and section [line: 4]. Again, we can nest as many (or 0) sectionGroups for further organization.
Using the values in our custom section is a little different than simply calling AppConfig:
1: NameValueCollection settings = (NameValueCollection)
ConfigurationSettings.GetConfig("CompanyCo/Portal");
2: int maxDepth = Convert.ToInt32(settings["maxDepth"]);First we get a
NameValueCollection (similar to a hashtable, but only supports strings) by calling the GetConfig and passing it the path of our section [line: 1]. Next we access the values by key name [line: 2].
All we've managed to do here is isolate our configuration values into their own little section. It's a great start, and key to understanding how sections work. In the next section we'll really get things rolling.
What we are really after is creating our own configuration handler. This will allow us to cleanly abstract away the parsing logic, in addition to supporting richer objects. The .Net framework provides a very handy interface,
System.Configuration.IConfigurationSectionHandler, which defines a single method to help us do this. Here's an example:
1: public class SampleConfigurationHandler : IConfigurationSectionHandler {
2: public object Create(object parent, object context, XmlNode node) {
3: SampleConfiguration config = new SampleConfiguration();
4: config.LoadValues(node);
5: return config;
6: }
7: }From our point point of view this code doesn't do much. A new
SampleConfiguration class is create [line: 3] (we'll look at that shortly).
LoadValues is called [line:4 ] which is where the real work will get done, and the configuration is returned [line: 5].
Although the above code is pretty basic, it's actually the key to hooking up out custom handler. With this code, we can go back into our web.config and instead of telling .Net to let the
NameValueSectionHandler handle our section, we can tell it to use our
SampleConfigurationHandler:
1: <configSections>
2: <sectionGroup name="CompanyCo">
3: <section name="Portal"
type="ConfigurationSample.SampleConfigurationHandler,ConfigurationSample" />
4: </sectionGroup>
5: </configSections>In case you aren't familiar how type's work in .Net, their string representation (which is what we are using here), is in the format of {FullNamespace}.{ClassName},{AssemblyName}. Don't add the .DLL to the {AssemblyName}. You can also add additional information, such as the Version, Culture and Public Key. You can read up more on AssemblyName's here.
Next we can create a much nicer section in our web.config:
1: <CompanyCo>
2: <Portal recursiveCopy="true" maxDepth="5">
3: <servers>
4: <clear />
5: <add name="BlueBell" address="10.1.1.2" isSource="true" />
6: <add name="CottonCandy" address="10.0.22.3" isSource="false" />
7: <add name="Butterscoth" address="10.0.22.4" isSource="false" />
8: <add name="Snuzzle" adress="10.0.22.5" isSource="false" />
9: <add name="Minty" address="10.0.22.6" isSource="false" />
10: </servers>
11: </Portal>
12: </CompanyCo>Hopefully you are already considering this a cleaner and more flexible way to go about handling a collection - we aren't having to uniquely identify the keys with an integer. I also added a couple new values,
recursiveCopy and maxDepth, to showcase how we won't be exposing strongly-typed values (a boolean and an integer).
To get this going, we'll create our SampleConfiguration class and define all the fields and properties we'll need:
1: public class SampleConfiguration {
2: #region fields and properties
3: private ServerCollection servers;
4: private int maxDepth;
5: private bool recursiveCopy;
6:
7: public ServerCollection Servers {
8: get { return servers; }
9: set { servers = value; }
10: }
11:
12: public int MaxDepth {
13: get { return maxDepth; }
14: set { maxDepth = value; }
15: }
16:
17: public bool RecursiveCopy {
18: get { return recursiveCopy; }
19: set { recursiveCopy = value; }
20: }
21: #endregion
22:
23:
24: #region Constructors
25: public SampleConfiguration() {}
26: #endregionThere's really nothing special so far. But things get a lot more interesting in the
LoadValues():
1: internal void LoadValues(XmlNode node) {
2: XmlAttributeCollection attributeCollection = node.Attributes;
3: recursiveCopy =
Convert.ToBoolean(attributeCollection["recursiveCopy"].Value);
4: maxDepth = Convert.ToInt32(attributeCollection["maxDepth"].Value);
5: foreach (XmlNode c in node.ChildNodes) {
6: switch (c.Name){
7: case "servers":
8: LoadServers(c);
9: break;
10: }
11: }
12: }Now we start to see the fruits of our labor pay off. Remember, the
LoadValues is called from our Handler, and receives an XmlNode [line: 1] which is the node of our configuration. From this node we can access all values of our section. The first thing we do is assign recursiveCopy [line: 2] and maxDepth [line: 3] to our class's corresponding fields. Since these are simply attributes of our <Portal> element in the web.config, they are read off the attribute collection of the node.
Next we loop through any child nodes [line: 5]. This is where we could load any type of collection, strongly typed objects or other special processing. In this example we'll do two-in-one, as the
LoadServers method will populate our Servers' collection with strongly typed objects [line: 8].
1: private void LoadServers(XmlNode node) {
2: servers = new ServerCollection();
3: foreach (XmlNode server in node.ChildNodes) {
4: switch (server.Name){
5: case "add":
6: Server s = new Server(server.Attributes["name"].Value,
server.Attributes["address"].Value,
Convert.ToBoolean(server.Attributes["isSource"].Value));
7: servers.Add(s);
8: break;
9: case "remove":
10: servers.Remove(server.Attributes["name"].Value);
11: break;
12: case "clear":
13: servers.Clear();
14: break;
15: }
16: }
17: }The code in LoadServers() loops through each child of the <servers> node and does one of three things:
clears the collection the child is <clear> [line: 12], removes the specified server from the collection [line: 9], or adds it [line:6]. While we are both doing a collection and a strongly typed object, you can see how easy it would be to doing a single strongly-typed object by looking at what add [line: 5] does.
The last thing that we'll do is add a static/shared method to our class so that this section can easily be access from our code, this is just a little icing on the cake:
1: public static SampleConfiguration GetConfig{
2: get {return (SampleConfiguration)
ConfigurationSettings.GetConfig("CompanyCo/Portal");}
3: }
One of the goals of creating our own configuration class and handler was to make things easier on the developer. We accomplished this by encapsulating all our parsing logic into a class which exposed tidy properties. This could have been just as easily accomplished using the appSetting, but we wouldn't have solved the other problems, such as messy and fragile web.config and shared sections. Look at how easy accessing the values becomes:
1: int maxDepth = SampleConfiguration.GetConfig.MaxDepth;
2: bool recursiveCopy = SampleConfiguration.GetConfig.RecursiveCopy;
3: ServerCollection server = SampleConfiguration.GetConfig.Servers;You might be cringing at the multiple calls to
GetConfig, note however, that the .Net framework automatically caches the parsed section. Therefore,
LoadValues() will only be called once (or whenever the process recycles itself, which is true for all caching).
| You must Sign In to use this message board. | ||||||||
|
||||||||
|
||||||||
|
||||||||
|
||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 2 Sep 2004 Editor: Nishant Sivakumar |
Copyright 2004 by Karl Seguin Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |