Bojan Veljanovski's Tech Blog

Strongly typed AppSettings configuration in .NET Core with validation

.NET

The default way to access .NET app configuration is by using the IConfiguration object.

public class Service
{
private readonly IConfiguration _config;
public Service(IConfiguration config) => _config = config;

public void DoSomething()
{
var configValue = _config["ConnectionString"];
// ...
}
}

This approach can be error-prone in larger codebases because it uses magic-string keys. Common issues are:

Introducing the strongly typed configuration class "AppSettings"

To solve this, create a strongly-typed class that will serve as a model for your app configuration:

public class AppSettings
{
public string ConnectionString { get; set; }
public string AppName { get; set; }
public int AppVersion { get; set; }
}

Then, configure it on app startup:

var appSettings = new AppSettings();
configuration.Bind(appSettings); // copy IConfiguration key-value pairs to appSettings
services.AddSingleton(appSettings); // register it in the DI

And finally use it in your app code:

public class Service
{
private readonly AppSettings _appSettings;
public Service(AppSettings appSettings) => _appSettings = appSettings;

public void DoSomething()
{
var configValue = _appSettings.ConnectionString;
// ...
}
}

Accessing nested configurations

Let's say your configuration JSON contains the following structure:

{
"Smtp": {
"Host": "...",
"Port": "..."
}
}

You can access the nested configurations through IConfiguration like this:

// IConfiguration _config = ...
var configValue = _config["Smtp:Host"];

To make this work in your strongly-typed access approach, change the AppSettings class into this:

public class AppSettings
{
public SmtpSection Smtp { get; set; } = new SmtpSection();
// ...

public class SmtpSection
{
public string Host { get; set; }
public int Port { get; set; }
}
}

And then use it like this in your app code:

// AppSettings _appSettings = ...
var configValue = _appSettings.Smtp.Host;

Validating strongly typed configuration

Call the appSettings.Validate() method early in the app startup. It will trigger self validation, throwing an exception on errors.

var appSettings = new AppSettings();
configuration.Bind(appSettings);
appSettings.Validate(); // NEW: add this here
services.AddSingleton(appSettings);

Here is a generic implementation to detect missing configurations. NOTE: tweak, extend, and change it as per your configuration validation needs.

public class AppSettings
{
public string Property1 { get; set; }
public string Property2 { get; set; }
// ...

/// <summary>
/// Ensures all configuration properties have a value that is not null and not empty.
/// It does not validate if the format of each property value is correct.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public void Validate()
{
var missingProperties = DoValidate(this);

if (missingProperties.Any())
{
const string msg = "AppSettings - missing configs:";
throw new InvalidOperationException($"{msg} {string.Join(", ", missingProperties)}");
}
}

/// <summary>
/// Find any missing configuration values with recursion.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private List<string> DoValidate(object obj)
{
var missingProperties = new List<string>();

var props = obj.GetType().GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(obj, null);
if (value == null || value.ToString() == "")
{
missingProperties.Add(prop.Name);
}
else
{
// If the property value is a complex object, then check it's
// nested properties.
if (value is object and not (string or IEnumerable))
{
var nestedMissingProperties = DoValidate(value);
foreach (var missingProperty in nestedMissingProperties)
{
missingProperties.Add($"{prop.Name}.{missingProperty}");
}
}
}
}

return missingProperties;
}
}