How we handle application configuration
We recently overhauled the way we handle configurable settings within our application (server names, email addresses, polling frequencies, etc) . I’m going to present our solution below, but its new enough that I’d like to hear feedback on how others approach the problem.
The goal
We want a configuration solution with the following traits:
- No magic strings. The configurable values will ultimately be stored with an identifying string key, but we didn’t want client code to have to know and use that string. The potential for typos is high, and maintaining a parallel list of constants is tedious.
- Strongly-typed values. The configured values will likely be stored as strings in a configuration file, but we never want client code to know that. If the configurable value is numeric, the consumer should get a number. Forcing the consumer to deal with type conversion (and related error handling) would create a lot of duplicate, error prone, “ceremony” code that would obscure the essence of what it is trying to accomplish.
- Test friendly. Client code that depends on a configurable value should not be forced to have any “out of process” dependencies (database, file system, etc.). Ideally, we would like to avoid having to stub expectations or use a test fake for configuration. We would also like to avoid adding any “simple type” (int, bool, string) constructor arguments to our classes, since the automocker we use cannot handle them.
- Tooling friendly. We will have a large number of configurable values in many different places around the codebase. Our solution should make it easy to build tooling to generate a sample configuration to document all of the possible settings. We also want to be able to validate an existing configuration to make sure all necessary values have been provided.
Though not necessary, it would also be nice to have:
- Default values. Users should only have to configure the values that have no sensible default, or values they want to override. It should not be left up to the client code to handle checking for a configured value, or falling back to a hardcoded default. That’s more “ceremony” code, not to mention the fact that multiple clients that use the same configurable settings would all have to know the default.
- App.config/web.config Storing application configuration settings in an app.config or web.config is a well-established convention in the .NET world. It already has support for encryption, multiple file overrides, and tooling. We should use it if it doesn’t cause too much pain.
Our solution in action
To explain our solution, I’ll use the example of a service that associates an uploaded image with a user profile. Users might upload images of all sizes, and in many different formats, but we want to make sure they are all stored in the same size and format. We want to give owner’s of our application the ability to set the standard size and format, so they must be configurable. We also want to make sure the path where images are stored is configurable. Consider the following implementation:
public class AvatarService : IAvatarService { private readonly AvatarSettings _settings; private readonly IFileSystem _fileSystem; private readonly IImageService _imageService; public AvatarService(AvatarSettings settings, IFileSystem fileSystem, IImageService imageService) { _settings = settings; _fileSystem = fileSystem; _imageService = imageService; } public void SaveAvatar(User user, Stream originalImage) { var extension = _imageService.GetImageExtensionFromMimeType(_settings.PreferredMimeType); var pathToSave = Path.Combine(_settings.AvatarStoragePath, user.Id + extension); user.AvatarFilePath = pathToSave; var scaledImage = _imageService.ScaleImageWithCrop(originalImage, _settings.PreferredMimeType, _settings.DefaultWidth, _settings.DefaultHeight); _fileSystem.Write(scaledImage, pathToSave); } }
As you can see, in order for the class to make use of these externally configured values, it just needs to take an instance of AvatarSettings in its constructor. At runtime, the settings will be injected by our composition tool (StructureMap), just like the file system and image services. The AvatarSettings itself is a simple POCO class:
public class AvatarSettings : DictionaryConvertible { public AvatarSettings() { PreferredMimeType = System.Net.Mime.MediaTypeNames.Image.Jpeg; DefaultWidth = DefaultHeight = 64; } [ExpandEnvironmentVariables] public string AvatarStoragePath { get; set; } public int DefaultWidth { get; set; } public int DefaultHeight { get; set; } public string PreferredMimeType { get; set; } } public abstract class DictionaryConvertible { private readonly List<ConvertProblem> _problems = new List<ConvertProblem>(); public IEnumerable<ConvertProblem> Problems { get { return _problems; } } public void AddProblem(ConvertProblem problem) { _problems.Add(problem); } }
I’ll discuss the DictionaryConvertible base class later, but I included it here to show that it does not bring along any extra baggage that would hinder the testability of derived classes. When testing client code like the AvatarService, we can prepare a specific test context by setting configuration values directly on an AvatarSettings instance. This is more natural than stubbing values on a fake. If we are testing a scenario that is not impacted by the configuration values, we can just let the automocker pass in a default instance.
To set the configurable values for the application, we just need to add corresponding keys to the appSettings section of the app.config/web.config file:
<appSettings> <add key="AvatarSettings.AvatarStoragePath" value="%ALLUSERSPROFILE%DovetailCRMAvatars"/> <add key="AvatarSettings.DefaultWidth" value="96"/> <add key="AvatarSettings.DefaultHeight" value="96"/> </appSettings>
There are a few things worth noting here. First, I have not included a value for AvatarSettings.PreferredMimeType in the config file. If you look at the source for AvatarSettings, you’ll notice it is set in the constructor. That is how we declare default values. It allows us to avoid cluttering up the config file with a bunch of settings that have reasonable defaults, while still allowing the flexibility to override them if desired. This is illustrated by the DefaultWidth and DefaultHeight settings which have configured values that override the defaults from the constructor. Also notice that the DefaultWidth and DefaultHeight properties are declared as integers (even though the values are strings in the config file) so that client code does not have to do any type conversion.
The final thing to notice is the [ExpandEnvironmentVariables] attribute that decorates the AvatarStoragePath property. In addition to type conversion, the code that populates a settings object can use additional metadata to do further manipulation of the configured values. In this case, the value “%ALLUSERSPROFILE%DovetailCRMAvatars” will be converted to “c:ProgramDataDovetailCRMAvatars” before any client code ever sees it. This not only simplifies the client code, but eliminates duplication of logic when a settings object has more than one client.
How it works
So how does this all work? The Settings object needs to be populated from the configuration file, so we define an interface for this responsibility:
public interface ISettingsProvider { DictionaryConvertible PopulateSettings(DictionaryConvertible instance); }
Implementations take in an instance of a DictionaryConvertible derived type, and should return an instance of the same type with all of the settings populated. We have a single implementation, AppSettingsProvider, which gets values from the
The final step is to tell StructureMap how to create instances of the settings classes so that they are properly injected into consumers. In our Registry, we add:
For<AvatarSettings>() .Use<AvatarSettings>() .EnrichWith((session, original) => session.GetInstance<ISettingsProvider>().PopulateSettings(original));
This is a good example of making use of the advanced power of your composition tool (and a good argument against trying to make a Common Service Locator equivalent for service registration). We use the EnrichWith statement to tell StructureMap “when I ask you for an AvatarSettings instance, before returning it to me, you should first pass it through the PopulateSettings method of my configured ISettingsProvider.”
Of course, it wasn’t long before we started adding a number of these Settings classes to our codebase, and it became a tedious extra step to add this same registration code for each class. The next logical step was to create a custom type scanner:
public class SettingsScanner : ITypeScanner { public void Process(Type type, PluginGraph graph) { if (!type.Name.EndsWith("Settings") || !typeof (DictionaryConvertible).IsAssignableFrom(type)) return; graph.Configure(r => r.For(type).EnrichWith((session, original) => { return session.GetInstance<ISettingsProvider>().PopulateSettings((DictionaryConvertible)original); }).TheDefaultIsConcreteType(type)); } }
We could now remove the previous code from our Registry that registered a single class (AvatarSettings), and replace it with this code which has the same effect for ALL Settings classes in our codebase:</p>
Scan(x => { x.TheCallingAssembly(); x.With<SettingsScanner>(); });
Tooling support
One of our goals was that the configurable settings should be easy to document. With a growing number of Settings classes all over the codebase, it could be very easy to lose track of which values are needed in the configuration file. However, since all of the Settings class follow a convention (derive from DictionaryConvertible, and named with the “Settings” suffix), it is trivial to write a little utility to reflect over the code and write out a sample
public void Execute(string[] args) { var settingTypes = _container.Model.PluginTypes .Where(t => t.PluginType.Name.EndsWith("Settings") && t.PluginType.BaseType == typeof(DictionaryConvertible)) .Select(t => t.PluginType); dumpSettings(settingTypes, Console.Out); Console.WriteLine(); } private void dumpSettings(IEnumerable<Type> settingTypes, TextWriter output) { var xml = new XmlTextWriter(output) {Formatting = Formatting.Indented}; xml.WriteStartElement("appSettings"); settingTypes.Each(t => { var settings = Activator.CreateInstance(t); var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public); properties.Each(p => { var key = AppSettingsProvider.DefaultNamingStrategy(p); var value = p.GetValue(settings, null); xml.WriteStartElement("add"); xml.WriteAttributeString("key", key); xml.WriteAttributeString("value", value == null ? "" : value.ToString()); xml.WriteEndElement(); }); }); xml.WriteEndElement(); xml.Close(); }
Summary
All of this comes together to allow a very natural workflow when we decide we need to make a value in our code configurable:
- Create a new Settings class (or append to an existing class) that derives from DictionaryConvertible and add the properties for each of the configurable values we need. Optionally initialize the properties with a default value in the constructor.
- Add the Settings class as a constructor parameter in the client class that needs the configurable value.
- After the client class is done and tested, add an
entry to the application configuration file for each setting without a default value.