Click here to Skip to main content
15,879,535 members
Articles / Web Development / ASP.NET

Creating Custom Configurations

Rate me:
Please Sign up or sign in to vote.
4.47/5 (18 votes)
2 Sep 200410 min read 80.9K   1.7K   70   2
Create 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.

Table of Contents

Introduction

ASP.NET's introduction of the web.config file went a long way to filling the configuration hole we had to deal with in classic ASP. We actually went from having very little (global.asa was the closest thing) to a well-structured, change on the fly XML file. For the most part, the web.config file does the trick fine. However, there are some limitations and constraints with using it, which you are probably already aware of. What you might not know is how easy it is to surmount those issues by creating a far more flexible alternative.

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.

Built-In Configuration

Overview

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:

XML
 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="Butterscotch" />
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:

C#
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), placing the value where it can easily be changed is a pretty basic requirement.

Problems

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:

  1. AppSettings is a shared space. So if you sell a Forum that expects to find its database connection string in the 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.
  2. Nothing is strongly-typed. Every value in the appSetting is a string - if you want an integer, a date or a serialized object, you'll have to convert them from a string each time you access the values. Additionally, the 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.
  3. No support for complex objects. This ties in to the second point, but it's impossible to have complex objects (such as collections) in the AppSetting section. I ran into this limitation while writing code to allow our network admins manually sync 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 when 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:

C#
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.

Creating Your Own basic Configuration Section

The solution to all three problems that we'll be looking at is creating your own little space in the web.config file where you rule supreme. While the built-in configuration capabilities in .NET might be somewhat limited, like most other things, it definitely allows us to easily expand and do our own stuff. It's possible to use ASP.NET's own configuration parsers on our little section, but that would only solve the first issue (i.e., we'd still be stuck with strings and wouldn't be able to have collections). As a starting point, that's exactly what we'll do. But first, let's go over the basics of configuration sections.

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:

XML
 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="Butterscotch" />
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 when 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:

C#
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.

Creating Your Own Configuration Section and Handler

Configuration Handler

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:

C#
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 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:

XML
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 AssemblyNames here. Next, we can create a much nicer section in our web.config:

XML
 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" address="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).

SampleConfiguration

To get this going, we'll create our SampleConfiguration class and define all the fields and properties we'll need:

C#
 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:        #endregion

There's really nothing special so far. But things get a lot more interesting in the LoadValues():

C#
 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].

C#
 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 accessed from our code, this is just a little icing on the cake:

C#
1:        public static SampleConfiguration GetConfig{
2:           get {return (SampleConfiguration)
         ConfigurationSettings.GetConfig("CompanyCo/Portal");}
3:        }

Using It

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:

C#
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).

Download

The download is a single web page with a 1 that allows you to select one of three methods to get information from the web.config to bind to a repeater. The first two are bad ways you might solve the problem, the third one uses the elegant solution we talked about here.

History

  • 3rd September, 2004: Initial version

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.


Written By
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAdd and Remove functionalities are not added Pin
majortargets15-Dec-15 22:54
majortargets15-Dec-15 22:54 
GeneralThanks! Pin
anichin8-Jun-06 10:49
anichin8-Jun-06 10:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.