Configuration

When we develop ASP.NET Core web apps, we will quickly realize that we need to change a few settings at runtime. To give an example, consider a web application that makes API calls to the service. When you develop the web app, the service is deployed in some development server and when you deploy it in production, the service URL is different, so the URL should not be hardcoded in the application. Instead, it should be read from the configuration, so that it can be changed without having to recompile the code. Another often used example is that of database connection strings, if the app makes use of them. This section takes a look at how we can accomplish this in ASP.NET Core.

The ASP.NET Core configuration model has three main constructs of interest:

  • ConfigurationProvider
  • ConfigurationRoot
  • ConfigurationBuilder

As shown in the following screenshot, the map diagram is constructed from Microsoft.Extensions.Configuration:

ConfigurationProvider is abstracted from the developer and hence we would not see it, but it's good to know that there is support for multiple configuration providers. A few of the important ones are:

  • JSON file: Reads the JSON file in the app's Startup folder and configures the application
  • Command-line arguments: While launching the app, command-line arguments can be passed as a key value to configure the application
  • Environment variables: Setting the environment variables, the provider reads the environment variable and takes care of configuration
  • Azure Key Vault: We will cover this in later chapters when we discuss Azure, but as the name suggests, it is a key vault.

So in a single app, we can choose to read a few configurations from the JSON file, a few from the command line, and so on. When we add multiple providers, it's important to consider the order in which we add them, as that defines the order in which the configuration values are added to the underlying dictionary. Configuration values from later providers will overwrite values with the same key from earlier providers. Also, we can notice in the diagram that there is a GetReloadToken method in ConfigurationProvider and a Reload method in ConfigurationRoot as well. If we read the documentation (using F12 in Visual Studio), we know that the configuration system supports reloading the configuration without having to restart the web app, which is fantastic. Let's see how we can read the configuration. The default Startup.cs comes with the following code:

public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

Notice that we already have the IConfiguration interface injected in the Startup constructor, which sets the Configuration property of the Startup. Also notice that when we create an app from the template, appsettings.json is included by default. Essentially, the default code has already wired up JSON-based configuration for us and we can leverage its goodness without having to code anything extra.

To make effective use of the Configuration property, lets do F12 (go to definition) on IConfiguration and check out its properties. The following is the code that comes up in Visual Studio:

    //
// Summary:
// Represents a set of key/value application configuration
properties.
[DefaultMember("Item")]
public interface IConfiguration
{
//
// Summary:
// Gets or sets a configuration value. //
// Parameters:
// key:
// The configuration key. //
// Returns:
// The configuration value.
string this[string key] { get; set; }

//
// Summary:
// Gets the immediate descendant configuration sub-sections. //
// Returns:
// The configuration sub-sections.
IEnumerable<IConfigurationSection> GetChildren();

//
// Summary:
// Returns a Microsoft.Extensions.Primitives.IChangeToken that
can be used to observe
// when this configuration is reloaded. //
// Returns:
// A Microsoft.Extensions.Primitives.IChangeToken.
IChangeToken GetReloadToken();
//
// Summary:
// Gets a configuration sub-section with the specified key. //
// Parameters:
// key:
// The key of the configuration section. //
// Returns:
// The
Microsoft.Extensions.Configuration.IConfigurationSection. //
// Remarks:
// This method will never return null. If no matching sub-
section is found with
// the specified key, an empty
Microsoft.Extensions.Configuration.IConfigurationSection
// will be returned.
IConfigurationSection GetSection(string key);
}

So, we see that we can get the value of a key through the indexer property, passing in the key to look up. We can get children, get a reload token, or get a specific section by giving the key. This documentation makes our task of using the configuration extremely easy. Let's see an example. First, define the config that we want to read, as shown here:

{
"Book": ".NET Core 2.0 By Example",
"Genere": {
"Name": "Technical level 200"
},
"Authors": [
{
"Name": "Rishabh Verma",
"Experience": "10"
},
{
"Name": "Neha Shrivastava",
"Experience": "7"
}
]
}

The code to read the values would be:

var book = Configuration["Book"];
var genereName = Configuration["Genere:Name"];
var author1Name = Configuration["Authors:0:Name"];
var author1Experience = Configuration["Authors:0: Experience "];
var author2Name = Configuration["Authors:1:Name"];
var author2Experience = Configuration["Authors:1:Experience"];

Notice that we used a zero-based index to access the contents of an array. The rest of the keys are read  just by passing in the correct key to the Configuration indexer, as we have just seen.

We generally use different configuration settings for different environments. For example, Development, Test, Staging, and Production may all have different configuration settings. The CreateDefaultBuilder extension method in an ASP.NET Core 2.0 app adds configuration providers for reading JSON files and environment variables in the following order:

  1. appsettings.json
  2. appsettings.<EnvironmentName>.json
  3. Environment variables

Here, appsettings.<EnvironmentName>.json would overwrite the key values defined in appsettings.json and the environment variables would overwrite the key values defined before them.

This didn't need any code changes in our Startup class. However, if we wish to use any other named config file, then we will need the code to be changed. The following example uses config.json as the configuration file, followed by config.Development.json:

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("config.json")
.AddJsonFile($"config.{env.EnvironmentName}.json");
Configuration = builder.Build();
}

A thoughtful programmer would say that in my appsettings.json, there may be a variety of sections for caching, database connection strings, service URLs, and so on. Why should I pass on all these values to every place? For example, what has caching to do with the database connection strings section or the service URL? Looks like there is no separation of concerns, and a clear violation of the I of SOLID: Interface Segregation Principle. Why should caching depend on IConfiguration when it needs just a subsection of it? To prevent these violations, it is not recommended to use the preceding way to access settings if configurations are to be read outside of Startup.cs.

We should instead use what is called the Options pattern. Using it is simple: first create a simple Plain Old CLR Object (POCO) class and then use it by registering it as a service and consume it wherever we need. It's a simple and dumb class with properties and no logic or smartness.

The POCO class for the preceding example would be:

public class BookDetails
{
public string Book { get; set; }
public Genere Genere { get; set; }
public Author[] Authors { get; set; }
}

public class Genere
{
public string Name { get; set; }
}

public class Author
{
public string Name { get; set; }
public string Experience { get; set; }
}

To register it, we go to the ConfigureServices method in Startup.cs and add the following line:

services.Configure<BookDetails>(Configuration);

BookDetails will be populated and added to the container. To use it in the controller, the following code would suffice:

private readonly BookDetails details;

public HomeController(IOptions<BookDetails> options)
{
this.details = options.Value;
}

The properties can now be used from the strongly typed object, details. If you wish to overwrite certain properties after the configuration is bound, you can do the following in the ConfigureService method:

services.Configure<BookDetails>(Configuration);
services.Configure<BookDetails>(opt => {opt.Name = “Roslyn via C#”;});

This changes the name of the book to "Roslyn Via C#". This is a last-one-wins approach, in which what is done last prevails. 

Another question that comes to mind is what if my configuration value changes, do I restart my app like in the old days when modifying the web.config used to restart the app? No, we have this covered in ASP.NET Core. For this, we need to use IOptionsSnapshot, which is designed to support the reloading of configuration data when the configuration file changes. Using IOptionsSnapshot with the reloadOnChange flag set to true, the options are bound to the configuration and reloaded when the file changes.

From the preceding sample, we just need to change IOptions to IOptionsSnapshot and add a flag, reloadOnChange, to true while adding the JSON file in the AddJsonFile method.

Let's take a step back and look at our Program.cs, which is the entry point of the application:

public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}

The CreateDefaultBuilder loads optional configurations from appsettings.jsonappsettings.{Environment}.json, User Secrets (in the Development environment), environment variables, and command-line arguments. The CommandLine configuration provider is called last. Calling the provider last allows the command-line arguments passed at runtime to override the configuration set by the other configuration providers called earlier, by the last-one-wins approach. Also, it is important to note that reloadOnChange is enabled for the appsettings file and so command-line arguments are overridden if a matching configuration value in an appsettings file is changed after the app starts.

When we work in enterprise applications, we will realize that there are a variety of configuration settings, such as secrets, passwords, and so on, that should not be kept in configuration files and hence should be kept out of the application code. Also, as a best practice, we should not be using production secrets in our development environment. In production also, these should be read from Azure Key Vault, which we will look at in later chapters. In the development environment, we can use the Secret Manager tool to safeguard secrets.

Before seeing how to use User Secrets, a question comes to mind: is that why we don't use environment variables to keep secrets away from the application, as it is supported by ASP.NET Core and is also there by default in the template code? Yes, we can use environment variables. However, environment variables are stored as plain text and can be accessed from any third-party code, so we can use them in local development but should not rely on them for production deployments.

The Secret Manager tool is just a fancy name given to store the secrets of a .NET Core project outside of the code base during development. The data stored is not encrypted. The only advantage this approach provides is that the secrets would not be part of the code and hence will not be checked in the source control, and so the secret will remain a secret during development. The secret is stored in a JSON file, which is kept in the user profile folder. To set up the User Secret tool, edit the .csproj file and add the following line in the ItemGroup node:

<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />

So the final code looks like this:

<ItemGroup>
<DotNetCliToolReference
Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools"
Version="2.0.0" />
<DotNetCliToolReference
Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0"
/>
</ItemGroup>

Save the .csproj file. This will restore the package we just added. Now, right-click the project in the Solution Explorer and select Manage User Secrets. This will add a new node named UserSecretId in the PropertyGroup. Save the file. A file named secrets.json will open up in Visual Studio. Hover over it and see the path. It will be %AppData%microsoftUserSecrets<userSecretsId>secrets.json.

Now we can add key value-pairs in the JSON file, just like we did for the configuration file. To read them, we need to add a couple of lines of code in our startup, as shown here:

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("config.json")
.AddJsonFile($"config.{env.EnvironmentName}.json");

Configuration = builder.Build();
}

After this, we can access the secret, just like we access anything else from the configuration:

var secretValue = Configuration["SecretKey"];

This concludes our configuration discussion. Let's check out how we can do logging.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset