2015/07/05

Content Management: Integrating System.Configuration

The .NET Framework comes with a library dedicated to application configuration, in the System.Configuration assembly. It's primary use, as intended by Microsoft, seems to be application level, design time configuration of the framework, but dynamic reading and writing, and user level configuration are also supported.
Using it is pretty simple: You create a file called App.config in your project and Visual Studio will recognize it, and copy and rename it appropriately at build time. The file itself is plain XML. Like most XML schemes, it is kind of verbose, annoying to write, and hard to read.
Similarly, the code to manually read it is a bit confusing, and you need to jump through hoops to make lasting changes to the config file. So our mission this time is to create a layer of abstraction above System.Configuration to alleviate some of these weaknesses. Since System.Configuration is not fully implemented in the Mono Framework, this might also come in handy if there is porting work to be done.

The abstracted API

For a given executable *.exe there is exactly one *.exe.config file, so there is very little sense in instantiating some class for this: multiple instances would just step on each other's toes. Using a static class is also more convenient, since no instances have to be passed around. It's easy to shoot yourself in the foot with global state (which is why I avoid static classes most of the time), but I think this is a pretty good use case for it.
We want to abstract away the complexities of loading the config file in the beginning of the program's life time, as well as the saving after reconfiguration by the user. So we will make two empty methods Load() and Save().
Finally, we don't want our game to crash because of an invalid or incomplete config file. We want to access config data in a type safe manner with default values to fall back on. To do that, we want clearly defined properties or methods on this class, rather than a key/value system.

Getting and setting values

There's at least four different ways to get at the actual values: There's the static ConfigurationManager, which you can use to make Configuration objects. You can use either of those to get string values or to deserialize classes derived from ConfigurationSection.
Sections are type safe and Configuration objects allow you to save changes back into the config file, so we will use that approach. There isn't a whole lot of code yet, so there isn't a lot of configuration data I could define, but I created a class DiagnosticsConfigurationSection : ConfigurationSection, and added a public property of that type to my static class. Note that this class can't be an inner type, for some reason. To make it serialize properly, you also have to add the [ConfigurationProperty] attribute to the properties you want it to serialize. This is also where you define default values and such.

Loading and saving App.config

Loading it isn't really difficult: Just create a private Configuration instance, which we will initialize in the Load() method, and route all your section properties to its GetSection() methods.
Writing it back is a bit tricky, however, and there seems to be this misconception that it isn't even supported. Makes you wonder why Configurstion.Save() exists. The root of this rumor, I suspect, is an issue with the debugger. See, what's called App.config in your project will actually be called *.config after build, where the wild card is replaced by the file name of the executable.
If you open up your debug build in the explorer, you'll notice that there is two executables (*.exe and *.vshost.exe), each with its own config file.
This is because Visual Studio has a hosting process running in the background, which, to my understanding, dynamically loads your executable for optimization purposes in the debugger. So, when you naively call Save() on your Configuration object, you will write to the right config file, but the wrong one is loaded on start up. Or vice-versa, depending on your point of view.
There's two solutions to this problem: disable the hosting process in your project settings, or load a config file other than App.config. If you opt for the former, you'll get a noticeable increase in start up time when debugging. The latter option is not better, I'm afraid: I didn't find a way to abuse this overload to load an arbitrary *.config file, so the only other files available are user level files in their AppData folder, which I would rather not clutter up.

Finishing touches

There's two things left for us to do: Write up the config file and actually use the configured data. The XML format is pretty well documented here, but here is the little file I have at the moment, as a quick reference:
The two values I defined there are used by my profiler and tracer collection. Integrating this into the profiler was pretty simple, just changing this line
callTree = new CallTreeNode[128];
to this
callTree = new CallTreeNode[
    Configuration.Diagnostics.CallTreeNodeMaxCount];

Configuring the TracerCollection was a little tricky because System.Diagnostics tracing isn't particularly well designed, or simply not comprehensive enough. I ended up implementing two new subclasses of TraceFilter, just to attach (1) multiple filters to a listener; and (2) white list multiple sources, rather than just the one allowed by SourceFilter.

Altogether, I feel like this is a pretty robust and intuitive system, and it took only one evening of lazy work to implement. The downside to this type safe approach is that it takes a bit of work for every configurable variable, but I feel like it's worth it, since you even get IntelliSense on the available config values.

No comments:

Post a Comment