In this blog post, we explore how we can configure settings. We start by answering the questions. What are configuration settings? Why do we need them? And how do we use configuration settings properly in .NET projects?
What are configuration settings and why do we use them?
With configuration, we define settings for our applications to be accessed in other parts of the code. Application configuration provides settings that are loaded during startup. These settings can differ from environment to environment, like your development, test and production environment. They also can change over time, for example, you have API keys that change once in a while. We want to separate this from our code, so we do not have source code changes, which requires recompilation and deployment of the application.
How do we use configuration settings?
When we create .NET Core 3.1 applications the NuGet package Microsoft.Extensions.Configuration
is referenced by default. This provides functionality to interact with configuration settings. There is also the Options pattern which uses classes to represent groups of related settings. With three examples we will explore how we retrieve configuration settings.
Example 1
Settings are defined as key-value pairs. The key is used to identify the setting and the value contains the data. Each configuration setting will have a unique key to access the configuration value at runtime. The settings are hierarchically organized. Related configuration can be grouped in sections, which we will see later. The hierarchy is flattened in order to access the configuration values. In this example we will see how the colon (:) separator to combine the sections in the hierarchy to locate the value.
In the application.settings.json
file, place the following section:
So the file looks like:
In this first example, we make use of dependency injection to inject the configuration in the HomeController. During Startup the default implementation of IConfiguration
will be retrieved and assigned to _configuration
.
Add IConfiguration configuration
as a parameter in the constructor and add the following field above the constructor.
private readonly IConfiguration _configuration;
Initialize the read-only field _configuration
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
The result should look like this:
Now we can read the BaseUrl from the application.json
file within the Index method.
First, we create the class Configurations.cs
in the Models
folder.
The BaseUrl
is within the section SiteConfiguration
which is within the section Example1
. To traverse the sections we make use of the colon :
. We have two sections so we use two :
, as in
We assign configurations
as the model in the line
The Index
method should look like
Now we can show the BaseUrl
in the view. Open Views\Index.html
.
We add the model on the first line
@model Configurations
We add
<p>BaseUrl: @Model.BaseUrl</p>
before the closing </div>
The Index.html
file will look like
Now you can start the project with ‘F5’ and see the result
Example 2
In this example, we will make use of Options pattern. We change the Configurations class in the Models
folder to
This mirrors the hierarchy of the sections in appsettings.json
.
In Startup.cs
in the ConfigureServices
method we add
services.Configure<Models.Example2>(Configuration.GetSection(nameof(Example2)));
With this line of code, we retrieve the section with the name Example
from appsettings.json
. Since the hierarchy and the naming is the same this is mapped correctly. We will see this later when we use it in the view.
In the HomeController
class, we add the parameter
IOptions<Models.Example2> settings,
The dependency container will give us an instance of the Example2
class, since we configured this in the Startup
class.
We add the following field above the constructor
private readonly Models.Example2 _example2;
and initialize it in the constructor. The result should look like
In the Index
method we create a Configurations
variable and assign _example2
to its Exampl2 property.
Now we can show the BaseUrl
in the view. Open Views\Index.html
.
We add at the first line
@model Configurations
We add
<p>BaseUrl: @Model.BaseUrl</p>
before the closing </div>
. The Index.html
file will look like
Now you can start the project with ‘F5’ and see the result
Example 3
In this example, we will validate when settings are missing. We start with the following settings in application.json
where BaseUrl
is commented out.
We change the Configurations.cs
to
In the class SiteConfiguration
we annotated the properties BaseUrl
and Key
with the Required
attribute. This will trigger an exception if the properties are not set. In order to make it clearer which property threw the error we add ErrorMessage
properties to the attributes.
In Example2 we used services.Configure<Models.Example2>(Configuration.GetSection(nameof(Example2)));
in Startup.cs
. In order to trigger the validation, we change this line in the method ConfigureServices
to
Pitfall 1: Since SiteConfiguration
is nested within Example3
in appsettings.json
, the following will not work.
We have to call explicitly "Example3:SiteConfiguration"
. Another option is to remove the Example3
outer section in appsettings.json
.
Then SiteConfiguration
is at the top-level.
Pitfall 2: Since the Required
attributes are on the properties within the class SiteConfiguration
will not trigger the validation.
In the Home
controller we have
In Index.html
we have
When we start the project, we will see
This is triggered when the Home
controller is called to serve the Index
page. If we had another controller and we opened a page on that controller first, then the Home
controller is not activated and will not throw a validation exception.
In order to trigger validation when the project is starting, we have to add the following changes.
- Add the
Microsoft.Extensions.Hosting
NuGet package to theExample3
project. - Implement the
IHostedService
interface to trigger the validation. - Add the
Hosted Service
to theConfigureServices
method inStartup.cs
.
Step 1
- Open the
Package Manager Console
, - Select
Example3
as theDefault project
and - Execute the line
Install-Package Microsoft.Extensions.Hosting
.
Step 2
We have to add another Controller in order to see that the error will not be triggered. I assume you know how to create a new Controller. We name the controller WeatherController
. The controller has only 1 method which returns the index.
The View
associated with the Index method is in the folder Views\Weather
and is named Index.cshtml
.
We change the launchSettings
to navigate to this page. We open Properties
and edit launchSettings.json
.
We add the line "launchUrl": "https://localhost:44369/weather/"
to the IIS Express
section.
When we start the project. We see that the application works fine. If we press the Home
-link, the familiar Error-page is shown. We want to check if the required settings are set during startup, which brings us to step 3.
Step 3
Create the folder Validations
. Create the class ValidateOptionsService
in it. Implement the interface IHostedService
. Generate the methods we have to implement.
public class ValidateOptionsService : IHostedService
Paste the following code in the class
The parameter IHostApplicationLifetime hostApplicationLifetime
is needed to stop the application when there are exceptions. This will be shown later.
The parameter IOptions<SiteConfiguration> settings
is the one we want to validate.
We added a logger to output the exceptions to the Output
window or we can log it in Application Insights
, which is outside the scope of this tutorial.
We implement the two methods StartAsync
and StopAsync
which the interface IHostedService
require
In Startup.cs
add the following lines to the method ConfigureServices
// Do not forget tot add new Settings to the ValidateOptionsService
services.AddHostedService<ValidateOptionsService>();
which results in
When we start the project, ValidateOptionsService
will be triggered and the required settings of SiteConfiguration
will be checked and in the Output
window we will see the following error:
Pitfall 3: If you add new settings, for example for your Storage
like Azure CosmosDB
. You create a StorageSettings
class and decorate the properties with the Required
attribute. In order to trigger the validation in ValidateOptionsService
, you have to add a parameter to the constructor
IOptions<StorageConfiguration> storageSettings
And trigger the validation in the StartAsync
method in the try
block with
_ = _storageSettings.Value;
Summary
In this tutorial, we have seen how we can read one setting in Example 1
:
_configuration["Example1:SiteConfiguration:BaseUrl"]
In Example 2
we have seen how we can map settings to classes. In Example 3
we have seen how we can validate required settings during the startup of the application.
In another tutorial, I will discuss UserSecrets
and the Azure KeyVault
because storing our secrets in source code is a severe vulnerability.
Code
You can find the code at Github.
Endnotes
I first came in contact with IOptions at a freelance project, later I learned about the PluralSight course Using Configuration and Options in .NET Core and ASP.NET Core Apps from Steve Gordon. Example 3 is based on this course. I am thinking about how to avoid adding a new IOptions<NewSettings>
each time in the constructor of ValidateOptionsService
. If you have a solution to how to solve this, I would like to hear.
You can read more at: