Why won't you load my configuration Azure Functions?

Loading configuration from local.settings.json didn't work how I was expecting it to so had to extend the start up to load structured configuration from appsettings.json

Published on 30 March 2021

Introduction

Configuration for any application is a must. This stops developers hard coding values, keeps secret values out of source control and allows different values to be used in development and production. However coming from an ASP.NET Core background the configuration setup for Azure Functions isn't quite as I would have expected out of the box. In this post we will take a look at the problem and see how we can solve it.

The Problem

Once you create an Azure Function it will generate a few json files for you. One is the host.json and this is where you can configure your functions host. The other is local.settings.json which will be ignored from source control (assuming git is used) as standard. This is where I thought the configuration should be put but I was wrong.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  }
}

The local.settings.json which is created will look similar to the above. It will define the storage account to be used and the functions runtime.

As part of an Azure Function which has a TimerTrigger it requires a Cron expression to define how often the function should fire. This can be hardcoded into the tigger code however values like this are ideal to be set through configuration. TimerTriggers also have the ability to reference a configuration key to get access to the value at runtime. This is similar to indexing straight into IConfiguration in ASP.NET Core but is wrapped with %. I've not looked into it but I assume this allows for the runtime to know it needs to look in the configuration system for the value.

Attempt #1

I wanted the configuration to be structured so I added it after the "Values" section in the local.settings.json file and ran the application. The setting could not be found. I setup the function to have IConfiguration injected through depencency injection into the constructor to try and index into the settings but there was no setting.

Attempt #2

After some Googling it looked like it required the values to be flat and setup like Environment Variables in the "Values" section of the configuration. This would have the value set next to FUNCTIONS_WORKER_RUNTIME however this felt quite restrictive to me. I do appriciate that there should probably be limited configuration for functions so it could be ok. As I'm learning about Azure Functions it also feels like environment variables are the current preferred method for setting values. It still didn't quite sit right with me though.

Attempt #3

Azure Functions don't come with Dependency Injection setup as default. Well actually that's not entirely true. There is a default set of services which are registered and various conventions which are used eg. you can inject an ILogger into a static function method, however it's not extensible straight away. To allow for you to register your own services and extend the default service registrations you have to create your own Startup class with similar concepts to ASP.NET Core. Now this will change in future versions when Azure Functions will become console applications like ASP.NET Core however with version 3 it is not the case.

To create your own Startup class and allow for configuring the DI Container etc. you need to create a class which inherits from FunctionsStartup.

A additional nuget package needs to be added to reference this class.

<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" /> 

This class implements a number of interfaces which hook into the web jobs startup functionality and has a couple of methods on it. The Configure method is abstract and hence required to be implemented. This gives you access to the IFunctionsHostBuilder and with that the Services property which exposes the IServiceCollection which should be familiar to any ASP.NET Core developer.

The other method which is a virtual method and hence optional is the ConfigureAppConfiguration method. This is where you can add any customisations for the configuration builder and add in other configuration providers as you wish.

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    FunctionsHostBuilderContext context = builder.GetContext();

    builder.ConfigurationBuilder
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
        .AddEnvironmentVariables();
}

In here we can add in additional configuration providers to mimic ASP.NET Core and have a core appsettings.json and then have environment specific versions eg. development. This value is found through accessing the EnvironmentName property on the configured context.

Using this method to add in additional json providers allows for adding in structured configuration and access it as you'd expect. It is important to add in the environment variables again at the end as this should always be the last provider to be loaded. Environment variables should always be loaded last, as last one wins, to allow for changes to be tweaked depending on environment.

This example code was taken from https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection.

Solution

Using the above example to add in the structured configuration through an appsettings.json file I was then able to add in my configuration as I wanted.

{
  "Schedule": {
    "Function1": {
      "CronExpression": "0 */2 * * * *"
    }
  }
}

This allowed for keeping all the "Schedule" based configuration items together and then key the confguration off the name of the function. Please note the appsettings.json file will not be copied to the build output as standard and this will need to be configured in the properties of the file (assuming Visual Studio is being used).

This configuration can now be referenced through the TimerTrigger itself.

[FunctionName("Function1")]
public static void Run([TimerTrigger("%Schedule:Function1:CronExpression%")]TimerInfo myTimer, ILogger log)
{
    log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
}

Conclusion

In this post we have walked through different ways of getting configuration loaded into your Azure Functions application and allow for structured configuration to be used in the TimerTrigger in the Azure function.

Any questions/comments/tips then please get in contact on Twitter @WestDiscGolf as I am still learning and would love to hear about pros and cons of using configuration in this way.