Application Configuration Files Explained
Rockford Lhotka
Magenic Technologies
April 15, 2003
Summary: Rocky Lhotka discusses XML configuration files in terms of the .NET Framework, and then shows you how to create custom sections using either pre-existing .NET section handlers or by building your own. With a bit more work, these custom section handlers can automatically read new values from the configuration file when files are changed. (16 printed pages)
Download the VBConfig.exe sample file.
If you've done any ASP.NET Web development, you're familiar with the web.config file. The web.config file is an XML file that contains the settings for the Web application, including security, debugging, tracing, and other behaviors. You may have also discovered that you can use this file to store application-specific configuration as well.
It turns out that every .NET application can have an XML configuration file. This includes Web applications through web.config, and also Microsoft Windows® applications through a .config file for each application. By using an XML configuration file, we can store application-specific configuration settings for any application in a common manner.
While the configuration file for a Windows application is often referred to as the app.config file, the actual file name is based on the name of the application itself. For instance, if the application name is myprogram.exe, then the configuration file name will be myprogram.exe.config. This file must be in the same directory as myprogram.exe.
The reason that the file is often referred to as the app.config file is that in Visual Studio® .NET, the project file is actually named App.config. When we build the project, Visual Studio .NET creates a copy of App.config in the bin directory with the appropriate name based on the application file name.
I'll be referring to app.config files throughout this article. Just remember that an app.config file actually refers to a file named based on your application name, residing in the same directory as the EXE file.
There is also a system-wide machine.config file that is used to configure common .NET Framework settings for all applications on the machine. In this article, we'll keep our focus on application-level configuration files because that is where we'll typically be working as we build, deploy, and configure our Windows and Web applications.
Note As a general rule, it's not a good idea to edit or manipulate the machine.config file unless you know what you are doing. Changes to this file can affect all applications (Windows, Web and other types) on your computer.
Sections
Configuration files are divided into sections, with each section being a top-level XML element. The root-level XML element in a configuration file (either web.config or app.config) is named <configuration>. This means that an empty configuration file would look like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>
If this is a web.config file, it will typically have a section to control ASP.NET:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<!-- settings go here -->
</system.web>
</configuration>
Notice how the name of the element is the same as the .NET Framework namespace we are controlling. ASP.NET functionality is found in the System.Web namespace.
This is consistent when adding sections to control various aspects of the .NET runtime. For instance, the <system.runtime.remoting> section is used to control remoting, which is found in the System.Runtime.Remoting namespace.
Most of these sections are accessed automatically by the .NET runtime as needed. Others, such as for remoting, are processed by the .NET runtime when told to do so by our application.
appSettings Section
One important section is the <appSettings> section. This section allows us to provide arbitrary configuration data to our application. The data is provided in a name/value scheme, and within our code we gain access to it through a (slightly modified) NameValueCollection object.
Within a config file, the <appSettings> section looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="name" value="the value">
</appSettings>
</configuration>
Remember that XML is case sensitive. Simple typos in configuration files will cause you problems.
This adds a new value named name with the value 'the value'. The add element is basically a command telling the configuration subsystem to add this new item. There are other commands as well:
Command | Result |
---|---|
Add | Adds a new setting value to the collection |
Remove | Removes a setting value from the collection |
Clear | Removes all setting values from the collection |
The reason the <remove> and <clear> commands are supported is because we can add system-wide settings in the machine.config file, and then we can remove or replace them at an application level if needed. Typically we just use the <add> command to add new settings for our application.
Within our application, we can access the settings from <appSettings> through the System.Configuration namespace. This namespace includes a ConfigurationSettings class that has Shared methods we can use to access configuration data. One of these methods is named AppSettings, and it provides access to the <appSettings> values:
Dim setting As String
setting = System.Configuration.ConfigurationSettings.AppSettings("name")
MsgBox(setting)
The result will be a message box with the text 'the value', since that is the value we set for this item.
It is important to realize that the entire <appSettings> section is read, parsed, and cached the first time we retrieve a setting value. From that point forward, all requests for setting values come from an in-memory cache, so access is quite fast and doesn't incur any subsequent overhead for accessing the file or parsing the XML.
A side effect of this is that changes to the configuration file won't take effect until the application is restarted. If you are Web developer you may take exception to this statement, because this isn't the behavior you'd observe in an ASP.NET application. This is because ASP.NET automatically detects any change to web.config and by design starts up a new version of the Web application with the new settings, allowing the previous version of the application to quietly disappear. Effectively, ASP.NET restarts the application for you behind the scenes.
Other developers aren't so lucky. Windows or console applications don't automatically detect changes to their app.config file, so any changes will only take effect after the application is restarted.
Section Handlers and Custom Sections
All these config file sections are parsed by components known as section handlers. A section handler is just a class that implements the System.Configuration.IConfigurationSectionHandler interface. The .NET Framework uses section handlers to read settings, such as those from <system.web> or <appSettings>.
We can also add our own custom sections to a config file. If we have componentized our application, we may want to have a different section to control each component. For instance, if we have a data access component, we may want a specific section in the config file to store settings to control data access.
The Framework also provides us with a set of handlers we can use if we want to add our own custom sections, or we can write code to implement our own custom section handler if none of the pre-built ones meet our needs.
To create our own custom section, we can choose from the following handlers in the System.Configuration namespace:
Handler | Result |
---|---|
NameValueSectionHandler | Returns a name/value collection of settings. Used to parse the <appSettings> section. |
IgnoreSectionHandler | Ignores all elements in the section. |
DictionarySectionHandler | Returns a Hashtable containing name/value settings. |
SingleTagSectionHandler | Returns a Hashtable containing name/value settings. |
We've already seen the XML needed to use the NameValueSectionHandler. We can use it to add our own custom section to the config file by adding a <configSections> section at the top of the config file. This section defines our custom sections:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="myCustomSection"
type="System.Configuration.NameValueSectionHandler,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089" />
</configSections>
<myCustomSection>
<add key="mykey" value="myvalue" />
</myCustomSection>
</configuration>
It is important to note that all sections in a config file must have a handler. If we include a section in a config file without an associated handler, the .NET runtime will throw an exception.
This is where the IgnoreSectionHandler comes into play. It allows us to add a section to the config file that is ignored by the .NET runtime when it loads configuration settings. You can then write code to manually open the file and read the XML to retrieve that data, without having to worry about the .NET runtime accidentally parsing it. Typically we won't use the IngoreSectionHandler, however. Instead, we'll use one of the three pre-built section handlers or we'll create our own custom section handler.
In our example we've defined a new section named <myCustomSection> and associated it with the NameValueSectionHandler. This means that our new section works like the regular <appSettings> section, but with a different section name. The <appSettings> section will still work as normal. Adding a new section doesn’t change the behavior of other pre-existing sections.
The DictionarySectionHandler expects to see XML in the same format as the NameValueSectionHandler, but it returns the values in a Hashtable rather than in a NameValueCollection. To define a dictionary section, we can use XML like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="myCustomSection"
type="System.Configuration.DictionarySectionHandler,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089" />
</configSections>
<myCustomSection>
<add key="mykey" value="myvalue" />
</myCustomSection>
</configuration>
Finally there is the SingleTagSectionHandler, which has a different format for the XML:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="myCustomSection"
type="System.Configuration.SingleTagHandler,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089" />
</configSections>
<myCustomSection mykey="myvalue" />
</configuration>
In this case, each attribute in the element is parsed as a name/value pair and is loaded into a Hashtable object.
So far we've focused on defining the XML in the config file to create each type of custom section. Now let's look at how we can write code to get at these values in our application. We've already used ConfigurationSettings.AppSettings to retrieve the standard <appSettings> values. To read custom sections, we need to use the ConfigurationSettings.GetConfig method, which returns settings for a specific custom section.
As with the <appSettings> section, the .NET runtime automatically caches the settings for each section as we access it. Thus, each section's data is only read and parsed one time, and the settings are kept in memory from that point forward.
The GetConfig method returns an object of type Object. This means that we need to convert it to the appropriate type by using CType (or the faster DirectCast). In our first example we defined a custom section that uses a NameValueSectionHandler. This handler returns a NameValueCollection object, so we can write the following code to retrieve the configuration settings:
Dim nv As Specialized.NameValueCollection
nv = DirectCast(ConfigurationSettings.GetConfig("namevalue"), _
Specialized.NameValueCollection)
txtNV.Text = nv("nvkey")
The GetConfig call returns an Object, which we convert into a NameValueCollection. We can then use the collection object in the same way we use the AppSettings property to retrieve values for each name.
In our second example, we defined a custom section using a DictionarySectionHandler. This handler returns a Hashtable object, so our code is a bit different:
Dim dict As Hashtable
dict = DirectCast(ConfigurationSettings.GetConfig("dict"), Hashtable)
txtDict.Text = CStr(dict.Item("dictkey"))
The end result is the same, but accessing a Hashtable is a bit different than accessing a NameValueCollection. The benefit to the Hashtable is that it allows much faster access to the setting values when we have a lot of settings. If we have just a few settings, the NameValue approach is best, but if we have more than a dozen or so settings in our section, the Dictionary approach is typically faster.
In our final example, we defined a section using the SingleTagSectionHandler, which also returns a Hashtable. The code to access this section is the same as for the DictionarySectionHandler:
Dim tag As Hashtable
tag = DirectCast(ConfigurationSettings.GetConfig("tag"), Hashtable)
txtTag.Text = CStr(tag.Item("tagkey"))
Custom sections can be very useful in segregating our application settings into groups, perhaps by component or any other meaningful grouping that meets your needs.
Creating a Custom Section Handler
While the .NET Framework provides us with three pre-built section handlers, sometimes they may not provide the functionality we require. In some cases we may have more complex configuration data than can be expressed by name/value pairs.
The <system.web> section is a good example of this situation. If you look at the nature of the data in this section, you'll see that it doesn't fit neatly into a simple name/value scheme. We may easily find that a complex business application has equally complex configuration requirements.
To address this need, we can create our own custom section handler that can parse arbitrarily complex XML within a custom section. Creating a section handler is only as difficult as parsing the XML. All we need to do is implement the System.Configuration.IConfigurationSectionHandler interface. This interface defines a single method, Create, which we must implement with our XML parsing code.
The Create method is passed an XmlNode parameter that contains the top-level node for our section. We can write standard XML code to work with that XmlNode and all the XML elements it contains.
The real trick is that the Create method returns a single value of type Object. It is up to us to figure out how to represent all our configuration values within the context of a single object that can be returned from the Create method.
In the case of the NameValueSectionHandler, a NameValueCollection object is returned. Likewise, the DictionarySectionHandler and SingleTagSectionHandler objects return a Hashtable as a result.
We can return a collection object, an array, or any other object we choose as our result, as long as it can provide access to the configuration settings. If we are creating our own custom section handler, it is probably because a simple collection-based result wasn't powerful enough to meet our needs. This means that we'll typically be returning an object of our own design as a result of the Create method.
As an example, let's create a custom section handler that can handle the following custom section:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="simpleSection"
type="MyAssembly.SimpleSectionHandler,MyAssembly" />
</configSections>
<simpleSection appName="my application">
<Node1>test value</Node1>
</simpleSection>
</configuration>
In this case we have an attribute on the main section node, plus we have a list of name/value data where the name is the node name and the value is the node value. This is quite different from the pre-existing section handlers, so we obviously need to create a custom handler.
First though, we need to create a data object that can hold our settings values. This is a regular class that will be created and populated with data from our section handler code:
Imports System.Collections.Specialized
Public Class SimpleSectionData
Private mAppName As String
Private mData As New NameValueCollection()
Public ReadOnly Property AppName() As String
Get
Return mAppName
End Get
End Property
Default Public ReadOnly Property Setting(ByVal Name As String) As String
Get
Return mData(Name)
End Get
End Property
Public Sub New(ByVal AppName As String, _
ByVal Data As NameValueCollection)
mAppName = AppName
mData = Data
End Sub
End Class
The class defines an AppName property and an indexed property that can be used to retrieve the individual setting values. It also has a constructor that initializes these values based on the data provided by the custom handler itself.
A custom handler is a class that implements IConfigurationSectionHandler. It only has one method, the implementation of Create:
Imports System.Configuration
Imports System.Collections.Specialized
Imports System.Xml
Public Class SimpleSectionHandler
Implements IConfigurationSectionHandler
Public Function Create(ByVal parent As Object, _
ByVal configContext As Object, _
ByVal section As System.Xml.XmlNode) As Object _
Implements System.Configuration.IConfigurationSectionHandler.Create
Dim data As New NameValueCollection()
Dim root As XmlElement = CType(section, XmlElement)
Dim node As XmlNode
For Each node In root.ChildNodes
data.Add(node.Name, node.InnerText)
Next
' create and return the settings container
Return New SimpleSectionData( _
root.GetAttribute("appName"), _
data)
End Function
End Class
Notice how the Create method is passed an XmlNode parameter corresponding to the top-level node for our section. All we need to do is write standard XML code to parse the node and its sub-nodes to pull out their data.
This code could be arbitrarily complex. The more complex the XML within the section, the more complex this parsing code becomes. In the end however, the business developer will never deal with the XML or the parsing code. Instead, all they'll see is the refined data from the object we return from the Create method.
In this case, we initialize and return a SimpleSectionData object, so the business developer only sees an object with AppName and Setting properties.
When we set up the custom section, we specified our SimpleSectionHandler class as the handler for <simpleSection>. When our application code tries to access this section through the GetConfig method, the .NET runtime automatically creates an instance of our SimpleSectionHandler class and calls our Create method. The object we return from the Create method is returned to the calling code.
As with the other configuration data we've discussed, our custom data will be cached by the .NET runtime, so the SimpleSectionHandler will only be invoked one time to read a section. Whatever object we return from the Create method will be cached in memory by the .NET runtime until the application terminates.
To use the configuration data, we can write code like this:
Dim settings As SimpleSectionData = _
CType(ConfigurationSettings.GetConfig("simpleSection"), _
SimpleSectionData)
txtAppName.Text = settings.AppName
txtNode1.Text = settings("Node1")
As with the standard .NET handlers, we call the GetConfig method to get the setting values. In this case, however, the return value is not a collection but instead is an object of type SimpleSectionData. Once we have the SimpleSectionData object, we can use its properties to retrieve whatever settings data were read from the config file.
Detecting Config File Changes
A couple times now we've noted that the .NET Framework automatically caches the settings data by caching whatever object is returned from the Create method of each section handler. This is typically a welcome feature because it automatically optimizes our code by avoiding unnecessary file IO and XML parsing. For ASP.NET applications there is no real downside because any change to web.config triggers the process of starting a new application and termination of the existing application. We automatically gain access to any new or changed setting values.
For other application types, such as Windows Forms or console applications, there is a definite downside. Any change to our configuration settings requires that the application be stopped and restarted. We can overcome this by adding a bit of extra functionality to our custom section handler. It is possible to create a custom section handler that detects when the config file is changed. If this happens, we can refresh our settings values by re-reading the custom section from the file.
Note that we cannot change the behavior of the existing .NET section handlers. This means that any use of sections such as <appSettings> or <system.runtime.remoting> will not be affected. However, we can make our own custom sections refresh automatically when the underlying configuration file is changed.
The secret lies in the fact that the .NET Framework caches the object returned from the Create method of our section handler. In our SimpleSectionHandler, we parsed the XML in the handler and then used the data to load the SimpleSectionData object. Once the SimpleSectionData object was loaded with data, the data became static and unchanging.
To support automatic updating of the data, we need to move the XML parsing from the handler into the data object itself. Since it is the data object that is cached by the .NET Framework, it is the only object that remains in memory. If the data object itself parses the XML, then it can also detect when the underlying file is changed and can re-read the XML if needed. This simplifies the section handler:
Imports System.Configuration
Public Class WatchingSectionHandler
Implements IConfigurationSectionHandler
Public Function Create(ByVal parent As Object, _
ByVal configContext As Object, _
ByVal section As System.Xml.XmlNode) As Object _
Implements System.Configuration.IConfigurationSectionHandler.Create
' create and return the settings container
Return New WatchingSectionData(section)
End Function
End Class
Since the data object will now handle the XML parsing, this code simply creates a data object (WatchingSectionData) and passes it the XmlNode object corresponding to this section. All the real work will happen in the data object.
The data object’s constructor accepts the XmlNode and calls a method to parse it:
Public Sub New(ByVal Section As XmlNode)
LoadXML(Section)
SetupFileWatcher()
End Sub
It also sets up a FileSystemWatcher object to watch the config file for changes. We'll discuss this later in more detail. For now, let's continue with the flow of XML parsing, which happens in the LoadXML method:
Private Sub LoadXML(ByVal Section As XmlNode)
mData = New NameValueCollection()
Dim root As XmlElement = CType(Section, XmlElement)
Dim node As XmlNode
mRootName = root.Name
mAppName = root.GetAttribute("appName")
For Each node In root.ChildNodes
mData.Add(node.Name, node.InnerText)
Next
mIsDataValid = True
End Sub
This code is basically equivalent to the parsing code we had in the SimpleSectionHandler class earlier. In this case, however, it parses the XML and uses the data to load private variables within the WatchingSectionData object itself so the code is actually a bit simpler.
Note that we also set an mIsDataValid variable to True. This variable indicates that the data in the object is current and valid since we've just read it from the file.
At this point our data object is loaded and ready to go. It is returned to the .NET Framework from the handler's Create method and is cached in memory. The business code gets a reference to this object and uses it like any other object.
But remember that we configured a FileSystemWatcher object through the call to SetupFileWatcher. This method gets the configuration file name from the AppDomain and parses the file name into a path and file name. We then use that information to set up a FileSystemWatcher component to watch the file:
Private Sub SetupFileWatcher()
' parse the file path and name
Dim info As New FileInfo( _
AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
Dim path As String = info.DirectoryName
Dim file As String = info.Name
' now set up the filesystemwatcher
Try
mWatcher = New FileSystemWatcher(path)
mWatcher.Filter = file
mWatcher.NotifyFilter = NotifyFilters.LastWrite
mWatcher.EnableRaisingEvents = True
Catch ex As Exception
Throw New Configuration.ConfigurationException( _
"Unable to initialize FileSystemWatcher for configuration file", _
ex)
End Try
End Sub
This code has extra error handling because it is possible that a FileSystemWatcher can't be set up for the file. An error can occur, for instance, if the application is running using no-touch deployment and the configuration file resides on a remote Web server.
In such a case there's nothing we can do. We can either ignore the exception and continue on, or do what this sample code does, which is to re-throw the exception with a descriptive message.
The mWatcher variable is declared using WithEvents. It will raise an event when the config file is changed, so we can handle this event to invalidate our data:
Private Sub mWatcher_Changed(ByVal sender As Object, _
ByVal e As System.IO.FileSystemEventArgs) _
Handles mWatcher.Changed
mIsDataValid = False
End Sub
All we do here is set the mIsDataValid to False to indicate that the data we currently have is no longer valid. Notice that we don't immediately re-read the file. This is intentional because it avoids a couple issues.
First off, we have no way of knowing if the application will ever use our configuration settings again. If they don't ever ask for a configuration value, then there's no point in re-reading the file and reparsing the XML.
Secondly, the FileSystemWatcher component will often raise several events for a single change to a file. If we re-read the file on each event, we could easily end up re-reading the file several times for a single change to the file.
Instead, we set mIsDataValid to mark our data as being invalid. On the next access from the client code, we'll check this variable and trigger a re-read of the data. For instance, when the AppName property is accessed we check the value:
Public ReadOnly Property AppName() As String
Get
If Not mIsDataValid Then LoadConfig()
Return mAppName
End Get
End Property
If mIsDataValid is False we call LoadConfig to re-read the data. This method opens the config file, navigates to the XmlNode for our section, and then calls LoadXML:
Private Sub LoadConfig()
Dim doc As New XmlDocument()
doc.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
Dim nodes As XmlNodeList = doc.GetElementsByTagName(mRootName)
If nodes.Count > 0 Then
LoadXML(nodes(0))
Else
Throw New Configuration.ConfigurationException( _
"Configuration section " & mRootName & " not found")
End If
End Sub
Thus, when the config file is changed while our application is running, we mark our data as being invalid. The next time the application asks for any configuration value we automatically reload the configuration data from the file.
Conclusion
The .NET Framework provides integrated support for XML configuration files for all applications. The configuration file is divided into sections, some containing configuration settings for the .NET runtime, others for our application. We can create custom sections for our application by using either the pre-existing .NET section handlers, or by creating our own custom section handlers. With a little extra effort we can even create a section handler that automatically reads new values from the configuration file when the file is changed.
Rockford Lhotka is the author of Expert One-on-One Visual Basic .NET Business Objects and numerous other books and articles. He speaks at several major conferences around the world. Rockford is the Principal Technology Evangelist for Magenic Technologies, one of the nation's premiere Microsoft Gold Certified Partners.