In this solution, we explore how we can configure settings. The examples, make use of .NET Core 3.1.
In all examples we make use of the configuration provider from Microsoft.Extensions.Configuration
.
In the application.settings.json
file, place the following section:
"Example1": {
"SiteConfiguration": {
"BaseUrl": "https://www.example1.com",
"Key": "ExAmPlE1KeY"
}
}
So the file looks like:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Example1": {
"SiteConfiguration": {
"BaseUrl": "https://www.example1.com",
"Key": "ExAmPlE1KeY"
}
},
"AllowedHosts": "*"
}
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 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:
private readonly IConfiguration _configuration;
private readonly ILogger<HomeController> _logger;
public HomeController(
IConfiguration configuration,
ILogger<HomeController> logger)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
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.
public class Configurations
{
public string BaseUrl { get; set; }
public string Key { get; set; }
}
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
var configurations = new Configurations
{
BaseUrl = _configuration["Example1:SiteConfiguration:BaseUrl"]
};
We assign configurations
as the model in the line
return View(configurations);
The Index
method should look like
public IActionResult Index()
{
var configurations = new Configurations
{
BaseUrl = _configuration["Example1:SiteConfiguration:BaseUrl"]
};
return View(configurations);
}
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
@model Configurations
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome in Example 1</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<p>BaseUrl: @Model.BaseUrl</p>
</div>
Now you can start the project with 'F5' and see the result
In this example, we will make use of Options pattern. We change the Configurations class in the Models
folder to
public class Configurations
{
public Example2 Example2 { get; set; }
}
public class Example2
{
public SiteConfiguration SiteConfiguration { get; set; }
}
public class SiteConfiguration
{
public string BaseUrl { get; set; }
public string Key { get; set; }
}
This mirrors the hierarchy of the sections in appsettings.json
.
"Example2": {
"SiteConfiguration": {
"BaseUrl": "https://www.example2.com",
"Key": "ExAmPlE1KeY"
}
}
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
private readonly Models.Example2 _example2;
private readonly ILogger<HomeController> _logger;
public HomeController(
IOptions<Models.Example2> settings,
ILogger<HomeController> logger)
{
_example2 = settings.Value;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
In the Index
method we create a Configurations
variable and assign _example2
to its Exampl2 property.
public IActionResult Index()
{
var configurations = new Configurations
{
Example2 = _example2
};
return View(configurations);
}
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.chtml
file will look like
@model Configurations
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome in Example 2</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<p>BaseUrl: @Model.Example2.SiteConfiguration.BaseUrl</p>
</div>
In this example, we will validate when settings are missing. We start with the following settings in
appsettings.json
where BaseUrl
is commented out.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Example3": {
"SiteConfiguration": {
//"BaseUrl": "https://www.example3.com",
"Key": "ExAmPlE1KeY"
}
},
"AllowedHosts": "*"
}
We change the Configurations.cs
to
public class Configurations
{
public Example3 Example3 { get; set; }
}
public class Example3
{
public SiteConfiguration SiteConfiguration { get; set; }
}
public class SiteConfiguration
{
[Required(ErrorMessage = "The BaseUrl is required for the SiteConfiguration section")]
public string BaseUrl { get; set; }
[Required(ErrorMessage = "The Key is required for the SiteConfiguration section")]
public string Key { get; set; }
}
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
services.AddOptions<SiteConfiguration>()
.Bind(Configuration.GetSection("Example3:SiteConfiguration"))
.ValidateDataAnnotations();
Pitfall 1: Since SiteConfiguration
is nested within Example3
in appsettings.json
,
the following will not work.
services.AddOptions<SiteConfiguration>()
.Bind(Configuration.GetSection(nameof(SiteConfiguration)))
.ValidateDataAnnotations();
We have to call explicitly "Example3:SiteConfiguration"
. Another option is to remove the Example3
outer section in appsettings.json
.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"SiteConfiguration": {
//"BaseUrl": "https://www.example3.com",
"Key": "ExAmPlE1KeY"
},
"AllowedHosts": "*"
}
Then SiteConfiguration
is at the top-level.
Pitfall 2: Since the Required
attributes are on the properties within the class SiteConfiguration
services.AddOptions<SiteConfiguration>()
.Bind(Configuration.GetSection(nameof(Example3)))
.ValidateDataAnnotations();
will not trigger the validation.
In the Home
controller we have
private readonly SiteConfiguration _siteConfiguration;
private readonly ILogger<HomeController> _logger;
public HomeController(
IOptions<SiteConfiguration> settings,
ILogger<HomeController> logger)
{
_siteConfiguration = settings.Value;
_logger = logger;
}
public IActionResult Index()
{
var configurations = new Configurations
{
Example3 = new Models.Example3()
};
configurations.Example3.SiteConfiguration = _siteConfiguration;
return View(configurations);
}
In Index.html
we have
@model Configurations
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome in Example 3</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<p>BaseUrl: @Model.Example3.SiteConfiguration.BaseUrl</p>
</div>
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.
public class WeatherController : Controller
{
// GET: Weather
public ActionResult Index()
{
return View();
}
}
The View
associated in the Index method is in the folder Views\Weather
and named Index.cshtml
.
@{
ViewData["Title"] = "Weather";
}
<h2>title</h2>
<p>What a beautiful day</p>
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.
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "https://localhost:44369/weather/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
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
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IOptions<SiteConfiguration> _settings;
private readonly ILogger<ValidateOptionsService> _logger;
public ValidateOptionsService(
IHostApplicationLifetime hostApplicationLifetime,
IOptions<SiteConfiguration> settings,
ILogger<ValidateOptionsService> logger
)
{
_hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
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
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
_ = _settings.Value; // Accessing this triggers validation
}
catch (OptionsValidationException ex)
{
_logger.LogError("One or more options validation checks failed");
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
_hostApplicationLifetime.StopApplication(); // Stop the app now
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask; // Nothing to do
}
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
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<SiteConfiguration>()
.Bind(Configuration.GetSection("Example3:SiteConfiguration"))
.ValidateDataAnnotations();
// Do not forget tot add new Settings to the ValidateOptionsService
services.AddHostedService<ValidateOptionsService>();
services.AddControllersWithViews();
}
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;
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.
You can read more at: