The Motivation

Currently when you make an Azure Functions project you have a local.settings.json file where you setup environment rules to do with the runtime frameworks, CORs settings etc - you can also configure ‘application settings’ that will be exposed as environment variables during runtime.

There are three things I would like to change about this:

  • I would like to separate the concerns by keeping environment configuration in one place, and my application/logic specific settings elsewhere.
  • It is not best practice to distribute sensitive settings by committing them into the repository - we need a different way of doing this. Currently you would have to commit the local.settings.json to distribute the sensitive values.
  • Once local.settings.json contains no sensitive values I’d like to remove it from the .gitignore so that each member of the team will have the same local configuration.

Setup

Enable User Secrets

User secrets are a tool built into .NET to allow developers to store secret information outside of the project root. It makes it less likely that secrets are accidentally committed to source control.

Right click on the project in Visual Studio and select Manage User Secrets - this will add any required NuGet packages and alter the project file where necessary.

Once you have followed the required steps you should be able to click on Manage User Secrets again and an empty secrets.json file will open. This indicates that user secrets has been correctly setup.

Add appsettings.json file

In the root of your project create an appsettings.json file and setup the insensitive values you want to store. Here is an example.

// appsettings.json

{
  "ConnectionStrings": {
    "MyConnectionString": "" // This value will be stored in user secrets - hence empty
    // (but we put it in here to remind us there is actually a value somewhere!)
  },
  "General": {
    "RandomColour": "blue",
    "Shape": "triangle"
  }
}

You also need to make sure that the appsettings.json file is set to copy to your build output:

<None Update="appsettings.json">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Add sensitive settings to User Secrets

You can see above that we store insensitive values directly in appsettings.json, but we will merge together our user secrets and these values to create our final configuration that the application has access to.

Open your secrets.json file and put your sensitive settings inside. E.g:

// secrets.json

{
  "ConnectionStrings": {
    "MyConnectionString": "my-sensitive-connection-string-1234-abcd"
  }
  // Note we didn't need the General section as this is taken from appsettings.json
}

Create options classes

To access the settings during runtime we will use dependency injection of different Options classes, we need to make these classes.

For our examples above we will create two classes, one for each of the sections; ConnectionStrings and General:

Configuration/
    ConnectionStrings.cs
    General.cs
// ConnectionStrings.cs

namespace MyProject.Configuration
{
  public class ConnectionStrings
  {
    public string RandomColour { get; set; }
    public string Shape { get; set; }
  }
}
// General.cs

namespace MyProject.Configuration
{
  public class General
  {
    public string MyConnectionString { get; set; }
  }
}

Install required packages for Dependency Injection

We need to install some NuGet packages to make sure that we will be able to inject our Options classes into our functions:

  • Microsoft.Azure.Functions.Extensions
  • Microsoft.NET.Sdk.Functions ( >= 1.0.28 )
  • Microsoft.Extensions.DependencyInjections ( <= 3.x )

Create functions setup class to configure everything

We will now create a startup class that will read values from our appsettings.json, our user secrets (and environment variables), merge them together into sections and then load into our preconfigured options objects. It will then finally register these options objects so our functions can request them via dependency injection.

The class should be called Startup.cs and be at the root of your project. Here is an example:

// Startup.cs

using System;
using System.IO;
using MyProject.Configuration;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(MyProject.Startup))]

namespace MyProject
{
    public class Startup : FunctionsStartup
    {
        private bool IsDevelopment =>
            string.Equals(Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT"), "Development", StringComparison.OrdinalIgnoreCase);

        /// <summary>
        /// Loads in settings from various sources including environment / user secrets / appsettings
        ///     and binds them to various options objects
        /// </summary>
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // Bind connection strings
            builder.Services.AddOptions<ConnectionStrings>().Configure<IConfiguration>((settings, configuration) =>
            {
                configuration.GetSection(nameof(ConnectionStrings)).Bind(settings);
            });

            // Bind general settings
            builder.Services.AddOptions<General>().Configure<IConfiguration>((settings, configuration) =>
            {
                configuration.GetSection(nameof(General)).Bind(settings);
            });
        }

        /// <summary>
        /// Defines the sources in which to load application settings from so they can be used above
        /// </summary>
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            FunctionsHostBuilderContext context = builder.GetContext();

            builder.ConfigurationBuilder
                .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true);

            if (IsDevelopment)
            {
                builder.ConfigurationBuilder.AddUserSecrets<Startup>();
            }
            else
            {
                builder.ConfigurationBuilder.AddEnvironmentVariables();
            }
        }
    }
}

Accessing the settings during runtime

You now need to request the options objects via dependency injection in your function. Here is an example:

// MyFunction.cs

...

private readonly IOptions<ConnectionStrings> _connectionStrings;
private readonly IOptions<General> _settings;

public MyFunction(IOptions<ConnectionStrings> connectionStrings, IOptions<General> settings)
{
    _connectionStrings = connectionStrings;
    _settings = settings;
}

...

// Access a setting
var shape = _settings.Value.Shape

Testing the application

If you now run the functions project locally you should find values stored in either user secrets or directly in appsettings.json are accessible at runtime! 🎉🎉

Deployment

User secrets are not supported when deployed, hence you need to move your sensitive values to environment variables when deployed. The values hardcoded into appsettings.json will still be read. You may have noticed this in Startup.cs that we merge from different locations depending on the environment.

There is just once small factor you need to be made aware of. You have have to flatten your JSON when naming your environment variables. For example the following setting in our user secrets:

{
  "ConnectionStrings": {
    "MyConnectionString": "my-sensitive-connection-string-1234-abcd"
  }
}

becomes the following environment variable (using double underscore__ to indicate nesting):

ConnectionStrings__MyConnectionString = "my-sensitive-connection-string-1234-abcd"

Documentation

When a new member of your team comes to work on your project they will not have their user secrets setup correctly so their project will not run as expected.

We suggest adding a section to your README that indicates what the layout of their secrets.json should be, and where they could get the value from.

// Please setup your secrets.json as follows,
// the connection string can be found in the company password vault.

{
  "ConnectionStrings": {
    "MyConnectionString": "<IN_PASSWORD_VAULT>"
  }
}

References