This is part three of a series exploring .NET Core configuration, with an emphasis on Azure Functions. In this article, we look at how we can add ASP.NET Core configuration into an Azure Function project and use IOptions<>
to inject strongly-typed configuration objects into our functions.
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
Should you even attempt to use ASP.NET Core configuration, using IOptions<>, in an Azure Function?
This may seem like a very odd question to bring up straight away, especially when this article seems to be presenting a working solution.
However, as a caveat before you rush to try this code out in your own solution, you really need to go into this with your eyes open, as the situation can get muddy very quickly.
If you go ahead and use the code in this article, you’ll end up with two somewhat disjointed configuration systems working in parallel. This isn’t ideal and will quite likely lead to confusion amongst your development team and conflict within your code.
However, an option that uses file-based configuration may be a solution that works well for some of you, particularly if you are working with a large number of separate configuration items, deployed in a number of different places.
As a reminder from part 1, this 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. This is why we draw parallels to ASP.NET Core throughout this article on Azure Functions.
ASP.NET Core Configuration. In part 1 of this series, we covered how ASP.NET Core provides a relatively flexible system of configuration. Specifically good reasons include:
- a baseline file-based configuration, in the form of the file
appsettings.json
, that deploys to all environments. - an abstraction of key-value pairs, that allow us to easily define structured configuration settings using json.
- support for binding of configuration to strongly-typed objects, using the Options Pattern.
- support for easy inclusion of those objects into our own code, using dependency injection of
IOptions<>
.
- a baseline file-based configuration, in the form of the file
Azure Functions Configuration. In part 2 of this series, we covered how Azure Functions were created to address several different requirements; they needed to be simple to use, cross-language and self-contained. They were not intended to require the additional complication of having to setup and configure the host (as we do with ASP.NET Core). We also talked about the fact that the runtime of Azure Functions has baked-in behaviour which means that some aspects of configuration need to be provided in a fairly prescribed way:
- each Function method is defined by an Attribute (that defines its type) that is hardcoded to accept certain parameters which are used by the runtime/bindings. These typically comprise of a setting name rather than the setting value.
- other configuration-settings, that are used within the body of the Function code itself, can be requested as needed, typically using
Environment.GetEnvironmentVariable("SomeConfigurationSetting")
- specific to the use of
local.setting.json
; configuration settings are expected to be located in theValues
node as flat key-value pairs. We cannot introduce nested JSON structures within theValues
node, nor can we use certain characters to define hierarchy. We can use a single-underscore to provide improved legibility. - out of the box, because there is no file-based baseline, all configuration needs to be defined repeatedly in different environments. i.e. all of the configuration-settings need to be listed in the
local.settings.json
- and then repeated again in the Azure AppService configuration. In a more complex distributed system,with duplicate instances of an AppService in multiple different Azure Regions, those configuration-settings need to be duplication for each instance.
- each Function method is defined by an Attribute (that defines its type) that is hardcoded to accept certain parameters which are used by the runtime/bindings. These typically comprise of a setting name rather than the setting value.
Alternatively, if we wanted to share a common source of configuration, a solution that is readily supported, is to use Azure App Configuration (we’ll cover this in part 4).
Does this even work?
ASP.NET Core and Azure Functions have approaches to configuration that are compatible - but they don’t really coexist together:
We can’t supply configuration to the Azure Functions runtime without using the provided method Attributes. As just mentioned, those Function Attributes (usually) dictate that we supply string literal arguments for setting-names, not setting-values. There is no opportunity to take advantage of strongly-typed
IOptions<>
here.The baked-in behaviour means that the runtime will only be looking for configuration in the places that it supports, such as
local.settings.json
. The Azure Functions runtime cannot see configuration from file sources that we define ourselves in the host startup.
To illustrate how this becomes a problem, let’s return to the sample code for CosmosDBTrigger
, that we looked at earlier in part2 of the series. We’re going to try to combine it with an unexpected configuration source. In this code sample, if we replace the CosmosDBTrigger
attribute parameter at line 7 with ConnectionStringSetting = "cosmosConnectionString"
For reader convenience, here’s that code again with the change highlighted:
public static class Function1
{
[FunctionName("Function2")]
public static void Run([CosmosDBTrigger(
databaseName: "databaseName",
collectionName: "collectionName",
ConnectionStringSetting = "cosmosConnectionString",
LeaseCollectionName = "leases")]
IReadOnlyList input, ILogger log)
{
if (input != null && input.Count > 0)
{
log.LogInformation("Documents modified " + input.Count);
log.LogInformation("First document Id " + input[0].Id);
}
}
}
It would now be fair to assume that next, we should add a corresponding setting into the root node of our [ASP.NET Core style] appsetting.json
file, like this:-
{
"cosmosConnectionString": "<an-actual-connectionstring>",
"anotherUnrelatedConfigurationSetting" : "anotherValue"
}
For the sake of this example, let’s assume that this appsetting.json
file has been correctly configured in the host startup, using the steps that we’ll describe a little later in this article. We should also assume that we haven’t confused matters by defining a duplicate of the setting, in a location that is expected, such as local.settings.json
. The purpose of this test is to attempt to use, solely, the appsettings.json
file.
If we now go ahead and try and run this, the Azure Functions will throw a wobbly, as the runtime does not know about this configuration source, even though we defined it in the host startup.
It’s easy to sound flippant when referring to “the baked-in behaviour” of the Azure Functions runtime, but we should be really mindful not to trivialise what Azure Functions, as a wider platform, is actually doing behind the scenes.
According to this GitHub issue, which discusses a scenario that is very similar to the one presented in this article, Microsoft Developer Fabio Cavalcante offered this advice which is totally worth quoting here, as it provides further insight into how the platform works:-
”…some infrastructure pieces, particularly in the consumption model, need to monitor trigger sources and some configuration options that control the Function App behavior. One example of such component is the scale controller. In order to activate and scale your application, the scale controller monitors the event source to understand when work is available and when demand increases or decreases. Using Service Bus as an example; the scale controller will monitor topics or queues used by Service Bus triggers, inspecting the queue/topic lengths and reacting based on that information. To accomplish this task, that component (which runs as part of the App Service infrastructure) needs the connection string for each Service Bus trigger setup and, today, it knows how to get that information from the sources we support. Any configuration coming from other providers/sources is not visible to the infrastructure outside of the runtime.“
How could this approach using supplemental configuration sources be useful to me?
As an example of where additional settings could be useful, let’s refer back to the code sample for a CosmosDbTriggered
Function, back in part 2. In that example, provided by the MS templates, we can see that there are baked-in settings for wiring everything up to the CosmosDb listener.
It’s likely that we’ll want our function to perform a task such as adding a message to a completely separate ServiceBus Queue service. To make this work, we would need to supply configuration information relevant to that service. Traditionally at this point, we would be adding multiple separate lines of code into our Function that uses things such as Environment.GetEnvironmentVariable("ServiceBusUrl");
etc.
By instead supplying our Function code with a strongly-typed IOptions<>
class, we can benefit from the aforementioned advantages associated with using ASP.NET Core style configuration.
Do I have to use a separate AppSettings.json file?
We can choose any filename we like, it doesn’t specifically need to be appsettings.json
. If we wanted to, we could even identify local.setting.json
as the configuration source and shoehorn-in extra JSON nodes - I really wouldn’t recommend doing this though! The key to making this work is to correctly identify the file in the startup
class, using .AddJsonFile("appsettings.json", false)
, which is then read by the host.
However, be mindful that a key reason for adopting this approach is so that we have the option to use appsettings.json
as a baseline configuration that can be deployed to different environments.
Dependency Injection in Azure Functions
You can read the official documentation here: Use dependency injection in .NET Azure Functions
The Azure Functions team are always working super-hard on improving the platform. It was announced at Build 2019 that Azure Functions had been updated to support native dependency injection (DI).
In one fell swoop, many of the things possible in ASP.NET Core became available for use in Azure Functions also.
Originally, Azure Function classes were required to be static classes. One of the notable recent changes made to support DI, is that Azure Functions can now be created as instance methods.
This then means that, combined with the new native dependency injection framework, we can inject objects into the constructor of our Function class instances.
Of specific interest to us, is the ability to inject IOption<>
objects into out Function classes and work with them, just like we do with ASP.NET Core
Let’s do it!
1) Explicitly tell the compiler which version of .NET Core SDK to use
This demo was created using .NET Core v2 - specifically SDK 2.2.402 / Runtime 2.2.7, which was the last version released, prior to being superseded by the newer .NET Core v3.
When testing this demo, I had recently installed the .NET Core v3 SDK - which had just been made GA (at the time of writing). The default behaviour for the compiler is to use the most recent version of the SDK installed.
Using the .NET Core 3 SDK, I experienced an incompatibility/bug which caused the demo to fail during the build.
Very briefly, it was “blowing up” with the error : Could not load file or assembly 'Microsoft.AspNetCore.Mvc.Abstractions...
. I ran out of time attempting to fix this - but this may be a problem which is already resolved, by the time you read this article.
In the meanwhile, to work-around the problem, we need to install and target a specific version of the SDK.
To achieve this:
- download and install the last release of the v2.2.x SDK from Microsoft.
- after installing, you can verify which SDK’s you have installed by using the command
dotnet --info
- add a new file
global.json
into the solution-level folder of the demo (i.e. one above the project folder) and edit it with the following:-
{
"sdk": {
"version": "2.2.402"
}
}
2) Set Framework Version and Add Nuget Packages in the project
- Update the
TargetFramework
tonetcoreapp2.2
(edit the.csproj
)
We use this complete list of packages - versions used as indicated:
Microsoft.NET.Sdk.Functions
(v1.0.29)Microsoft.Azure.Functions.Extensions
(v1.0.0)Microsoft.AspNetCore.App
(v2.2.6)
Do not confuse
Microsoft.AspNetCore.App
withMicrosoft.NetCore.App
3) Update local.settings.json
with a setting
The goal here is not really to use local.setting.json
, but it will help to demonstrate that the configuration systems can work side-by-side in certain cases.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
"LocalSettingValue" : "Setting From local.settings.json"
}
}
4) Create appSettings.json
and populate
This will depend on your own project, but this is an example of what the configuration could look like:
{
"ConfigurationItems": {
"CommonValue": "Setting From appsettings.json",
"SecretValue": "Do not save secret values in appsettings"
}
}
You need to ensure that appsetting.json
will be included as a deployable artefact of the build:
If you are using Visual Studio, from Solution Explorer, right-click and select Properties. Under “Copy to Output Directory”, select “Copy always”.
Alternatively, for developers using other code editors, you can edit the
.csproj
like this:-
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
5) Create user secrets
- From the Solution Explorer, go ahead and right-click on the project file,
- Select “Manage User Secrets”. A new
secrets.json
file will open - populate it with the following code:
{
"ConfigurationItems": {
"SecretValue": "Secret Setting From User Secrets"
}
}
As a reminder from part 1 of this series, behind the scenes, the “Manage User Secrets” option will:
- create a new key within the
.csproj
file, with the nameUserSecretsId
and a new GUID value. - create a new folder in your OS user-profile which mirrors the GUID. Within this folder, is where you can find the actual
secrets.json
file - although Visual Studio makes it easy to open this.
6) Create a Model which will bind to configuration
We’re going to take our string-based configuration file and bind the values to a strongly-typed object. We should make a model class for this purpose:
public class ConfigurationItems
{
public string CommonValue { get; set; }
public string SecretValue { get; set; }
}
7) Create a startup class
A project created from an Azure Functions template is minimal and doesn’t include a Startup.cs
class, so let’s add that in next.
using System;
using System.Reflection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using FunctionApp1;
using FunctionApp1.Models;
[assembly: FunctionsStartup(typeof(Startup))]
namespace FunctionApp1
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var config = new ConfigurationBuilder()
//.SetBasePath(Environment.CurrentDirectory) ← do not use
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false)
.AddUserSecrets(Assembly.GetExecutingAssembly(), false)
.AddEnvironmentVariables()
.Build();
builder.Services.Configure<ConfigurationItems>(config.GetSection("ConfigurationItems"));
builder.Services.AddOptions();
}
}
}
Take care not to use .SetBasePath(Environment.CurrentDirectory)
. This is provided in most examples you’ll find on the web. This works fine for development, but does not work when you publish to Azure. Instead, you should use this: .SetBasePath(Directory.GetCurrentDirectory())
Also take care not to omit the [assembly: ...]
attribute, found adjacent to the namespace
section in the example above, otherwise your startup class will not actually be invoked at startup. The project will still build without it though, which could lead to confusion.
If you’ve been looking at other guides related to this subject on the web,, you may have found code examples which use
WebJobsStartup
instead ofFunctionsStartup
andIWebJobsBuilder
instead ofIFunctionsHostBuilder
… these are constructs that come with ASP.NET Core. As far as I can tell, the Azure Functions team has added aFunctions
variant in mid-April 2019, which looks to be a wrapper ofIWebJobsStartup
etc - check out the source code for azure-functions-dotnet-extensions.
8) In your Azure Function class
Finally, in our Function class, we can now inject your strongly-typed configuration options, like this:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using FunctionApp1.Models;
namespace FunctionApp1
{
public class Function1
{
private readonly ConfigurationItems _configurationItems;
public Function1(IOptions<ConfigurationItems> configurationItems)
{
_configurationItems = configurationItems.Value;
}
[FunctionName("Function1")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req
)
{
string localSettings = Environment.GetEnvironmentVariable("LocalSettingValue"); // Included so as to demo regular approach
string commonValue = _configurationItems.CommonValue;
string secretValue = _configurationItems.SecretValue;
return new OkObjectResult($"Local Value : '{localSettings}' | Common Value : '{commonValue}' | Secret Value : '{secretValue}'");
}
}
}
When working with an
IOptions<T>
type, don’t forget to include.Value
to get to the actual configuration settings.
9) Run the program
We should see output like this:-
Local Value : 'Setting From local.settings.json' | Common Value : 'Setting From appsettings.json' | Secret Value : 'Secret Setting From User Secrets'
In the final part of this series, we look at using other configuration services, specifically Azure App Configuration and Azure Key Vault.