A while ago (almost 4 years ago, to be precise) Ben Hall wrote a blogpost about using Castle DictionaryAdapter to build a simple, strongly typed wrapper aroud application settings.
While extremely simple, and gets the job done, there are a few ways it can be improved. With that, this blogpost can be treated as an introduction to Castle DictionaryAdapter (which sadly has precisely zero documentation).
While the apprpach is really simple, we’re going to take a detailed look and take it slowly, which is why this post is fairly long. Buckle up.
What is DictionaryAdapter
In a nutshell DictionaryAdapter is a simple tool to provide strongly typed wrappers around IDictionary<string , object>
Here’s a simple example:
// we have a dictionary...
var dictionary = new Dictionary<string, object>
{
{ "Name", "Stefan" },
{ "Age", 30 }
};
// ...and an adapter factory
factory = new DictionaryAdapterFactory();
// we build the adapter for the dictionary
var adapter = factory.GetAdapter<IPerson>(dictionary);
Debug.Assert(adapter.Name == "Stefan");
Wrapping app settings
While the project is called DictionaryAdapter in fact it can do a bit more than just wrap dictionaries (as in IDictionary
). It can be used to wrap XmlNode
s or NameValueCollection
s like Configuration.AppSettings
and this last scenario, is what we’re going to concentrate on.
The goals
We’ve got a few goals in mind for that project:
- strongly typed – we don’t want our settings to be all just
string
s
- grouped – settings that go together, should come together (think username and password should be part of a single object)
- partitioned – settings that are separate should be separate (think SQL database connection string and Azure service bus url)
- fail fast – when a required setting is missing we want to know ASAP, not much later, deep in the codebase when we first try to use it
- simple to use – we want the junior developer who joins the team next week to be able to use it properly.
With that in mind, let’s get to it.
Getting started
DictionaryAdapter lives in Castle.Core
(just like DynamicProxy), so to get started we need to get the package from Nuget:
Install-Package Castle.Core
Once this is done and you’re not using Resharper make sure to add using Castle.Components.DictionaryAdapter;
to make the right types available.
The config file
Here’s our config file, we’ll be dealing with. Notice it follows a simple naming convention with prefixes to make it easy to group common settings together. This is based on a real project.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="environment-type" value="Local" />
<add key="environment-log-minimum-level" value="Debug" />
<add key="auth0-client-id" value="abc123abc" />
<add key="auth0-client-secret" value="123abc123abc" />
<add key="auth0-domain" value="abc123.auth0.com" />
<add key="auth0-database-connection-name" value="ABC123" />
<add key="auth0-token-expiration-seconds" value="3600" />
</appSettings>
</configuration>
As you can see we have two sets of settings here. One for general environment configuration, and another one for integration with Auth0.
Config interfaces
Now let’s proceed to creating our interfaces, exposing the configuration values to our application. We follow a naming convention where the name of config value corresponds to type/property on the interface. This makes it trivial to see which config value maps to which property on which interface.
For clarity I’d also recommend putting the interfaces in a designated namespace, like MyApp.Configuration
.
public interface IEnvironment
{
EnvironmentType Type { get; }
LogEventLevel LogMinimumLevel { get; }
}
public interface IAuth0
{
string ClientId { get; }
string ClientSecret { get; }
string Domain { get; }
string DatabaseConnectionName { get; }
int TokenExpirationSeconds { get; }
}
Having the interfaces and the config we can start writing the code to put the two together
Bare minimum
Let’s rewrite the code from the beginning of the post to use our config file and interfaces. If you’re not using Resharper you’ll have to manually add a reference to System.Configuration.dll
for the following to work.
var factory = new DictionaryAdapterFactory();
var environment = factory.GetAdapter<IEnvironment>(ConfigurationManager.AppSettings);
Debug.Assert(environment.Type == EnvironmentType.Local);
If we run it now, we’ll get a failure. DictionaryAdapter doesn’t know about our naming convention, so we have to teach it how to map the type/property to a config value.
Implementing DictionaryBehaviorAttribute
There are two ways to customise how DictionaryAdapter operates.
In this example we’re going to use the latter. To begin, we need to create a new attribute, inheriting from DictionaryBehaviorAttribute
, and apply it to our two interfaces
public class AppSettingWrapperAttribute : DictionaryBehaviorAttribute, IDictionaryKeyBuilder
{
private readonly Regex converter = new Regex("([A-Z])", RegexOptions.Compiled);
public string GetKey(IDictionaryAdapter dictionaryAdapter, string key, PropertyDescriptor property)
{
var name = dictionaryAdapter.Meta.Type.Name.Remove(0, 1) + key;
var adjustedKey = converter.Replace(name, "-$1").Trim('-');
return adjustedKey;
}
}
We create the attribute and implement IDictionaryKeyBuilder
interface, which tells DictionaryAdapter we want to have a say at mapping the property to a proper key in the dictionary. Using a trivial regular expression we map NameLikeThis
to Name-Like-This
.
Having done that, if we run the application again, the assertion will pass.
Fail Fast
If we remove the environment-log-minimum-level
setting from the config file, and run the app again, it will still pass just fine. In fact, since LogEventLevel
(which comes from Serilog logging framework) in an enum, therefore a value type, if we read the property everything will seem to have worked just fine. A default value for the enum will be returned.
This is not the behaviour that we want. We want to fail fast, that is if the value is not present or not valid, we want to know. To do it, we need two modifications to our AppSettingWrapperAttribute
Eager fetching
First we want the adapter to eagerly fetch values for every property on the interface. That way if something is wrong, we’ll know, even before we try to access the property in our code.
To do that, we implement one more interface – IPropertyDescriptorInitializer
. It comes with a single method, which we implement as follows:
public void Initialize(PropertyDescriptor propertyDescriptor, object[] behaviors)
{
propertyDescriptor.Fetch = true;
}
Validating missing properties
To ensure that each setting has a value, we need to insert some code into the process of reading the values from the dictionary. To do that, there is another interface we need to implement: IDictionaryPropertyGetter
. This will allow us to inspect the value that has been read, and throw a helpful exception if there is no value.
public object GetPropertyValue(IDictionaryAdapter dictionaryAdapter, string key, object storedValue, PropertyDescriptor property, bool ifExists)
{
if (storedValue != null) return storedValue;
throw new InvalidOperationException(string.Format("App setting \"{0}\" not found!", key.ToLowerInvariant()));
}
If we run the app now, we’ll see that it fails, as we expect, telling us that we forgot to set environment-log-minimum-level
in our config file.
Wrapping up
That’s it. Single attribute and a minimal amount of bootstrap code is all that’s needed to get nice, simple to use wrappers around application settings.
Additional bonus is, that this is trivial to integrate with your favourite IoC container.
All code is available in this gist.