Read configuration data in .NET

Automatic parsing of configuration data from JSON into a DTO using the

ConfigureOptions
class.

Managing configuration settings in .NET applications offers numerous possibilities, yet often I find the available documentation to be less than ideal. This makes it challenging to identify the beste™ or at least a user-friendly method. In this article, I will outline how I have approached this issue.

The manual method

Numerous guides, both on the internet and on the Microsoft website, explain how to access configuration data using the
IConfiguration
class in C#. This data is provided through JSON, environment variables, and startup arguments. Often, this data is manually retrieved.
appsettings.json
{
	"Azure":
	{
		"ApiKey": "YouNeedToPushYourSecretKeyToAllRemotesIfYouHaventDoneSoAlready"
	}
}
Startup.cs
public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		string apiKey = Configuration["Azure:ApiKey"];
	}
}
The problem with this method is its susceptibility to errors. On one hand, it involves handling strings that do not provide type safety, and on the other hand, it can become particularly complicated when more complex data than simple strings is needed.

Complex data structures

The following JSON file is designed to be practical and includes elements that can cause headaches during manual parsing: various data types, including enumerations, POCOs and arrays.
appsettings.json
{
	"Localization":
	{
		"DefaultLanguage": "en",
		"Languages": [ "de", "en" ],
		"Providers": [ "JsonFileLocalizationProvider" ],
		"PageLocalization":
		{
			"HomePageRedirect": true,
			"RedirectStyle": "WithoutPrompt",
			"RedirectDelay": 0
		}
	}
}
This data structure, when implemented in C#, would look something like this:
LocalizationOptions.cs
public record LocalizationOptions
{
	public String? DefaultLanguage { get; init; }
	public List<String>? Languages { get; init; } = new();
	public List<String>? Providers { get; init; } = new();
	public PageLocalizationOptions PageLocalization { get; init; } = new();
}
PageLocalizationOptions.cs
public record PageLocalizationOptions
{
	public Boolean HomePageRedirect { get; init; }
	public RedirectStyle RedirectStyle { get; init; }
	public Int32 RedirectDelay { get; init; }
}
RedirectStyle.cs
public enum RedirectStyle
{
	WithoutPrompt,
	PromptUser,
	TooManyRedirects
}
Reading this structure can be extremely tedious (Pain in the Ass®). It's worth noting that in this example,
RedirectStyle.TooManyRedirects
particularly stands out as it causes the application to trigger the HTTP status code
310
without any apparent reason. This is a brilliant idea that should be implemented somewhere in every application.

Automatic Mapping

The mapping process can be simplified by using the class from the
Microsoft.Extensions.Options
. package. This approach involves deriving from the class and defining a lambda expression in the constructor. This delegate receives an instance of the DTO class as a parameter. Since the expression is specified in the constructor, an instance of
IConfiguration
is being captured and also in the scope. The
Bind()
method is then used to pass the path specified by a string (separated by
:
) in the configuration tree along with the instance of the options class, thereby writing the values into the object.
Startup.cs
Could not find file '/app/Features/Blog/Posts/DotNetOptionsSetup/Assets/LocalizationOptionsSetup-simple.cssnippet'.
To inform the Dependency Injection (DI) system that this options class is available, it is added to the
IServiceCollection
.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.TryAddEnumerable(
        ServiceDescriptor.Singleton<IConfigureOptions<LocalizationOptions>, LocalizationOptionsSetup>());
}
After this step, the class becomes available to other classes registered in the Dependency Injection (DI) container.
MyService.cs
public class MyService(IOptions<LocalizationOptions> options)
{
}

Advanced configuration

Since the container is already available at the time of setup, it is possible to request other services if specific actions are necessary for the proper construction of the options class. This provides flexibility and enhanced possibilities for customization.
LocalizationOptionsSetup.cs
public class LocalizationOptionsSetup(IConfiguration configuration, ISomeService someService)
	: ConfigureOptions<LocalizationOptions>(options =>
		Configure(options, configuration, someService))
{
	private static void Configure(LocalizationOptions options,
								  IConfiguration configuration,
								  ISomeService someService)
	{
		configuration.Bind("Localization", options);
		someService.DoImportantThings(options);
	}
}
In the next article, I will demonstrate how to use
IOptionsMonitor
to respond to changes in the configuration within the program. This allows for real-time updates, ensuring that your application can adapt dynamically to configuration modifications.

Share the post

React to configuration changes in .NET

React to configuration changes in .NET

OptionsMonitor
is a powerful tool that allows you to dynamically monitor configuration options and react to changes.