Customising AppSettings.json Bindings in NET6
Introduction
In .NET there is a common way of binding your configuration files into simple POCO objects and using them. This pattern is known as the Options Pattern and is well documented and advocated by Microsoft. This pattern allows us to easily convert json files, environment variables etc. into concrete configuration objects based on hierarchical/precedent rules and thus enabling us to easily override configuration per environment etc. or in tests.
However, I recently came across a little known feature that was brought in with .NET6 and recently saved me a lot of pain/refactoring of my codebase which I wanted to share here.
Options Binding
Just to set the scene for anyone that is unfamiliar with how Options work in .NET lets assume we have the following appsettings.json file in our solution:
{
"service": {
"setting1": "red",
"setting2": "blue",
"setting3": "green"
},
"aws": {
"dynamodb": {
"tableName": "myTable"
},
"s3" : {
"bucket": "myBucket"
}
}
}
When .NET reads the above file, what it will actually do behind the scenes is flatten the entire object graph into a simple Dictionary
or KeyValuePair
structure like so:
{
"service:setting1": "red",
"service:setting2": "blue",
"service:setting3": "green",
"aws:dynamodb:tableName": "myTable",
"aws:s3:bucket": "myBucket"
}
We can easily bind this in code by crafting a simple POCO like the following:
public record ConfigurationRoot
{
public Service Service {get; init;}
public Aws aws {get; init;}
}
public record Aws
{
public Dynamodb dynamodb {get; init;}
public S3 s3 {get; init;}
}
public record Dynamodb
{
public string tableName {get; init;}
}
public record S3
{
public string bucket {get; init;}
}
public record Service
{
public string setting1 {get; init;}
public string setting2 {get; init;}
public string setting3 {get; init;}
}
And then we just need some simple configuration binding code during our application startup:
Note: now for people doing ASPNET.Core some of this boilerplate is automagically done via the
WebHostBuilder.CreateDefaultBuilder()
documented here
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json") // Default configuration
.Build();
var myConfigObject = config.Get<ConfigurationRoot>();
This works nicely because .NET can crawl the json and match it up perfectly to my the concrete object model I defined. But notice that I have to have that AWS
wrapper class which doesn’t really do much, what if I wanted to get rid of that and flatted things?
You could do some separate binding of objects, using config.GetSection("aws").Get<DynamoDb>()
but this gets very complicated in larger configuration files and is prone to human error. Luckily there is an easier way.
ConfigurationKeyNameAttribute
Luckily the clever folks at Microsoft provide us a nice and simple way to provide custom binding paths via simple use of attributes.
So lets put this attribute to use in our example.
All we need to do is make some simple changes to our configuration POCO:
public record ConfigurationRoot
{
public Service Service {get; init;}
[ConfigurationKeyName("aws:dynamoDb")]
public Dynamodb Dynamodb {get; init;}
[ConfigurationKeyName("aws:s3")]
public S3 S3 {get; init;}
}
public record Dynamodb
{
public string TableName {get; init;}
}
public record S3
{
public string Bucket {get; init;}
}
public record Service
{
public string Setting1 {get; init;}
public string Setting2 {get; init;}
public string Setting3 {get; init;}
}
As you can see, I now no longer have that wasted Aws
class and I have provided a simple :
separated path to enable the binder to navigate my configuration. Simple.
TL;DR
- The
ConfigurationKeyNameAttribute
allows us to easily and conveniently override the default .NET Options binding convention. - By using this attribute we can massively simplify our configuration POCO objects and make them easier to use and understand
- We can reduce the need for extra “placeholder” classes/records
- Works for configuration loaded from any supported source (xml, environment variables etc.)
Hope this helps someone else as it did me 😃