[Configuration in Azure Functions Series - Part 1] Introduction to .NET Core configuration
Jim Mc̮̑̑̑͒G
Posted on December 9, 2019
This article was originally published at blogs.siliconorchid.com on 1-Nov-2019
This is part one of a series exploring .NET Core configuration, with an emphasis on Azure Functions. In this article, we introduce the subject of configuration in general and look at how ASP.NET Core configuration is recommended to be used.
In part 1 of this series, we introduce the subject of configuration and review how ASP.NET Core configuration works.
In part 2 of this series, we look at how configuration in Azure Functions (v2) works and talk about some of the issues.
In part 3 of this series, we show how you could include ASP.NET Core configuration into an Azure Function project.
In part 4 of this series, we look at using other configuration services, specifically Azure App Configuration and Azure Key Vault
Overview
Welcome to this series of four articles where we'll be covering the topic of application configuration, looking at both ASP.NET Core and Azure Functions v2.
The series has been written from the perspective of the developer who may already be accustomed to developing with ASP.NET Core, and is wanting to learn about the slightly different way that Azure Functions v2 goes about handling application configuration.
We'll cover how and why things are different, possible sources of confusion and provide some working examples. In the final part, we'll look at how to integrate powerful Azure services that can help to improve your application configuration.
Throughout this series, please be conscious that we will be referring to the following with deliberate distinction:
.NET (AKA ".NET Framework") is a Windows-only software framework, dating back to 2002.
ASP.NET is a server-side web application framework, that expands upon the original .NET framework.
.NET Core is a higher-performance, cross-platform and open-source software framework, introduced in 2016.
ASP.NET Core is a server-side web application framework, that expands upon the newer .NET Core software framework.
Azure Functions v1 is Microsoft's initial offering of a "serverless" platform, released in November 2016. It supports a number of languages, including .NET, but not .NET Core.
Azure Functions v2 is the second iteration of the "serverless" platform, released in September 2018. It shifted the underpinning software framework from .NET to .NET Core.
Unless specified otherwise, assume that any mention of Azure Functions in this series, relates specifically to v2
Introduction
Configuration - Where have we come from?
If you've worked as a developer using Microsoft technologies for a number of years, it is most likely that you will have developed web applications using the ASP.NET Framework. In which case, you will be familiar with the venerable web.config
file.
For the benefit of new developers, it may be useful to know that this configuration system dates all the way back to the release of the original ASP.NET, back in 2002. For a very long time now, it has been the defacto, convention-based, way to manage configuration.
Configuration in a web.config
file mixes server settings and application settings together in a file that looks partly like this:-
<configuration>
<appSettings>
<add key="connectionString"
value="Server=localhost;UID=sa;PWD=secret;Database=Northwind" />
</appSettings>
</configuration>
To access configuration settings in our application code, we would add something like the following, directly in the place where it was needed:
var connectionString = ConfigurationSettings.AppSettings( "connectionString" )
Configuration - Where are we now?
With the 2016 release of .NET Core and ASP.NET Core, the way application configuration was supported changed significantly and, as developers, the scope of what we needed to know about this subject broadened greatly.
This is particularly true when it comes to the topic of "where should different types of configuration information be stored - and where should it not be stored?".
With the relatively recent release of Azure Functions v2 in September 2018, Microsoft shifted from using the .NET Framework, to instead use .NET Core.
For the ASP.NET Core developer in particular, there is now a synergy that makes the adoption of Azure Functions a really appealing proposition.
However, when it comes to the subject of configuration, the shift from ASP.NET Core to Azure Functions isn't entirely frictionless. It is this main theme, that we will attempt to address in this series.
Whilst researching these articles, two observations became apparent:
Although Microsoft promotes a harmonised technology stack, there is a slight lack of cohesion/consistency between their products. This is noticeable when you are familiar with some of the conventions found in ASP.NET Core and arrive at Azure Functions expecting things to work in exactly the same way.
Because there are many options to tackle, what is essentially the same basic task, it's easy to feel overwhelmed with choice.
These observations are truly not intended as a criticism of Microsoft, but it is indicative that their products are created, maintained and supported by a large number of people across several different teams. Furthermore, each product requires many people to create and maintain comprehensive documentation, with a consistent and cohesive message that dovetails with other documents - this alone is not a trivial task!
This series is the outcome of having collated various sources of documentation together. Hopefully it will be a useful guide for developers shifting between, or integrating, the various technologies that we'll look at.
Modern .NET Core configuration, in general
If we distil the subject of configuration down to its most basic concept, the .NET Core libraries provide us with an API that offers a collection of key/value pairs.
Configuration information is combined together from separate layers, from one or more different sources known as Configuration Providers.
Where we require an environment-specific piece of configuration (e.g. a feature switch setting), or secrets (e.g. a sensitive API key to another service), we can supplement or replace a configuration resource with information from additional other sources of configuration.
Configuration Providers are (usually) registered in the initialisation code of our project and locate the required information under the direction of the application and the specific environment within which our program is running.
According to the official documentation, at the time of writing, the list of possible configuration providers include:
- Azure Key Vault
- Azure App Configuration
- Command-line arguments
- Custom providers (installed or created)
- Directory files
- Environment variables
- In-memory .NET objects
- Settings files
Configuration providers (for example Azure Key Vault) are integrated to our project by adding the appropriate Nuget Package and registering that provider into the WebHost Builder in
startup.cs
class. We'll explain exactly how to go about doing this, in part four of this series.
Configuration, the ASP.NET Core way
You can read the official and in-depth documentation regarding ASP.NET configuration here: Configuration in ASP.NET Core.
When we create a .NET Core WebApp (either MVC or WebAPI) - and assuming that we use the default templates - we'll find ourselves working with a file-based configuration-source called appsettings.json
.
Broadly speaking, appsettings.json
should be considered as the baseline configuration resource for our web application.
Configuration-settings in appsettings.json
are read using the File Configuration Provider - specifically the JSON Configuration Provider.
Use of this file-based source is not a baked-in behaviour of the ASP.NET Core runtime - it is facilitated by calling the method CreateDefaultBuilder
from within the application startup code program.cs
. This "default builder" loads configuration from the various sources in a certain order - refer to the section
Default configuration for more info.
appsettings.json
is structured JSON, so this allows us to freely add groupings to our configuration information. For example:
{
"EmailSettings": {
"ApiKey": "secretKeyValue",
"FromAddress": "do-not-reply@test.com"
},
"FeatureSwitches": {
"EnableFeatureOne": true,
"EnableFeatureTwo": false,
},
}
We can expect appsettings.json
to be deployed to all environments. For example, when we build/publish (locally or to Azure), this file is literally copied as a deployment artefact.
Because the configuration is layered, appsettings.json
is used first and is then overlaid with replacement values from other configuration providers, such as the User Secrets provider.
N.B. To clarify: when we create those additional configuration resources, we only need to specify the individual keys to be overwritten - we don't need to create an entire copy of each and every item in the baseline configuration.
Which options are commonly used with ASP.NET Core?
We listed all of our choices just a moment ago, but let's look a little more closely at some of the options that are commonly used:-
-
Environment-specific appsetting variants (file-based sources) such as
appsettings.development.json
orappsettings.production.json
, are an overlay of the baseline file-based configuration. They are used selectively at runtime, depending on the name of the Environment that has been set.As with the primary file,
appsetting.json
, these remain intended for non-secret settings and are included in version control.You can read about environments here at Microsoft Documentation : Use multiple environments in ASP.NET Core.
Environment-specific appsetting variants should not be confused with the similar-sounding Environment Variables. We do however need to define the "environment name" using the Environment Variable
ASPNETCORE_ENVIRONMENT
.We can set any name that we like, but the ASP.NET Core framework only natively supports three variants:
Development
,Staging
andProduction
. If no value is set,Production
is used as default.In development, you will typically find
ASPNETCORE_ENVIRONMENT
declared in the fileproperties/launchsettings.json
When we talk about "native framework support", we're referring to the fact that we have built-in features that allow us to react selectively to the stated environment. As examples of how this is consumed, consider this C# code in the
startup.cs
class and also an example of a Razor tag :-
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
...
<environment include="Development"><p>Development mode</p></environment>
<environment include="Staging"><p>Staging mode</p></environment>
To make use of these variants, we simply need to include appsettings.development.json
or appsettings.staging.json
files in our project. We can include appsettings.production.json
in our project if we want to purposefully separate production settings from a default baseline configuration.
Tip: If you cannot see these files in Visual Studio, be aware that the Solution Explorer collapses the variants - click the caret next to the file to expand the view:
-
User Secrets for local development secrets. The
secrets.json
file is stored outside of our project code, within our Operating System user profile. This keeps it completely away from version control and provides access control via our operating system.If you aren't familiar with User Secrets, head over to this beginner-friendly article for an introduction and step-by-step instructions.
If you create a new MVC/WebAPI project from the MS templates and right-click on the project file, you will find the option "Manage User Secrets".
If you repeat this exercise, by creating an empty Azure Function (or indeed, just a Console App), you will find the "Manage User Secrets" option doesn't display.
The key to enabling this menu option is the presence of the package
Microsoft.Extensions.Configuration.UserSecrets
. Adding either this package or theMicrosoft.NETCore.App
meta-package, to your project, will enable the option in the menu.Behind the scenes, when you use this menu item, VS will have added a new
<UserSecretsId>
node and Guid value into your.csproj
. At the same time, a new folder that matches the Guid, will have been created inside your user profile. Inside this folder we can expect to find the filesecrets.json
which will partly mirror the structure of theappsettings.json
.<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AzureFunctionsVersion>v2</AzureFunctionsVersion> <UserSecretsId>550b1379-cb4e-42ec-aa0c-72a5ac8f3700</UserSecretsId> </PropertyGroup>
C:\Users\<your username>\AppData\Roaming\Microsoft\UserSecrets\<UserSecretsId>
It is this separation of files, well away from the rest of your source code, that gives us a clear delineation between files that should be secret and those that should be in version control.
-
Azure AppService Application settings for PaaS cloud deployment. For a majority of scenarios, this is where all of our environment-specific configuration is defined when deployed to an Azure AppService.
If you aren't familiar with Azure AppService Application settings, check out Microsoft Documentation: Configure an App Service app in the Azure portal.
Azure AppService support the concept of separate "deployment slots", which offer us the option of multiple similar environments (e.g. production, UAT and a test versions) - each with slot-specific configuration if required.
Take care not to confuse "Azure AppService Application settings" with the similarly-named "Azure App Configuration", as this is another Azure service. We'll look at this service in part 4 of this series.
With regards to secrets specifically, we could safely add these configuration items into Azure AppService Application settings. There is nothing inherently insecure about doing this (from an external-intrusion perspective) as values are encrypted at rest.
However, a potential concern is that anyone with legitimate permission to access the Azure AppService, will also be able to view these secrets. This may be an issue depending on your organisation (a problem solved by using Azure Key Vault, which we'll look at in part 4 of this series).
Tip: As an alternative to editing values directly into the Azure Portal, this AzureTipsAndTricks article shows us how to update Azure settings, in Visual Studio, when publishing.
Keep it secret. Keep it safe!
The appsettings.json
file can be expected to be included in version control, so it must not include secrets (e.g. things such as a database connection string that includes a login credential).
Although the actual secret value should itself not be included in version control, it is recommended that you still retain the configuration-item key in the appsettings.json
. Instead of providing the actual secret value, we can add a message with some variant of "do not include here").
- By doing this, we can use
appsetting.json
as a working manifest of sorts, listing all expected configuration items for the project, along with the expected structure. Although we provide unusable key-values, we can trust that this will still work fine, as the actual working values will be substituted at runtime. - This provides a way to clearly communicate amongst your development team, which configuration items are supposed to be present. It also could be said that regularly seeing this message serves to reinforce the need for the development team to think about where they store configuration items.
To illustrate this, an appconfig.json
and secrets.json
pair could look like this:-
Firstly, the appconfig.json
:-
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ConnectionStrings": {
"DefaultSql": "Do not specify here. Set in Azure AppConfiguration or User Secrets",
"AzureStorage": "UseDevelopmentStorage=true"
},
"EmailSettingsConfig": {
"ApiKey": "Do not specify here. Set in Azure AppConfiguration or User Secrets",
"FromAddress": "do-not-reply@test.com"
}
Combined with a secrets.json
:-
{
"ConnectionStrings": {
"DefaultSql": "Server=tcp:[serverName].database.windows.net;Database=myDataBase;
User ID=[secretDbLogin]@[serverName];Password=secretPassword;Trusted_Connection=False;
Encrypt=True"
},
"EmailSettingsConfig": {
"ApiKey": "secretAccountKey"
}
Strongly-typed Configuration models
A really nice feature that ASP.NET Core introduced, alongside the use of appsettings.json
, was support for the Options Pattern.
This lets us bind nodes within configuration files to strongly-typed C# objects (models) and then make them available for consumption by our application using the IOptions
interface.
By using dependency injection, we can conveniently insert these populated models into the constructor of classes as required. If you are familiar with the subject of unit testing, you will appreciate that this is a very good thing.
Benefits of this way of working include:
- It's super easy to organise/group related configuration items together, by using hierarchy to nest them under a parent item in the config file.
-
The configuration binding code can typically be confined to a single place in our project (e.g. the startup class), avoiding code sprawl.
- String literal bugs (e.g. the silly mistakes that we all make, such as typo's in a key name) are reduced or even avoided. Where they are a problem, they tend to show up sooner in the development process. This is because an error in a centralised part of the codebase tends to be noticed quickly, because it usually impacts many parts of our application. This is when compared to having our wider codebase littered with references to configuration that rely on string literal values.
When we start working with strongly-typed configuration objects, rather than crude string values, we can then start to do useful things such as using dependency injection to insert populated configuration models into our consuming code.
We'll examine the actual code for all of this, in part 3 of this series
Update Nov2019 : Shortly after publishing this article, Steve Collins of stevetalkscode.co.uk reached out, as he has recently been speaking and blogging about the subject of configuration himself. You can find his slide deck here. You may find it useful to explore his blog about configuration-bridging, as it covers the subject of using
IOptions<>
compared to directly binding an object to configuration.
In part 2 of this series, we look at how configuration in Azure Function works and talk about some of the issues.
Posted on December 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 9, 2019
December 9, 2019
December 9, 2019
December 9, 2019