11

I have an Azure function, and I'm using the DI system in order to register some types; for example:

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services
            .AddTransient<IMyInterface, MyClass>()
    . . . etc

However, I also was to register some data from my environment settings. Inside the function itself, I can get the ExecutionContext, and so I can do this:

IConfiguration config = new ConfigurationBuilder()
   .SetBasePath(context.FunctionAppDirectory)
   .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
   .AddEnvironmentVariables()
   .Build();

However, in the FunctionsStartup, I don't have access to the ExecutionContext. Is there a way that I can either get the ExecutionContext from the FunctionsStartup class or, alternatively, another way to determine the current running directory, so that I can set the base path?

Paul Michaels
  • 16,185
  • 43
  • 146
  • 269

5 Answers5

44

While the checked answer to this question is correct, I thought it lacked some depth as to why. The first thing you should know is that under the covers an Azure Function uses the same ConfigurationBuilder as found in an ASP.NET Core application but with a different set of providers. Unlike ASP.NET Core which is extremely well documented (https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) this is not the case for Azure Functions.

To understand this set of providers you can place the following line of code in the Configure(IFunctionsHostBuilder builder) method of your FunctionStartup class,

var configuration = builder.Services.BuildServiceProvider().GetService<IConfiguration>();

place a debug break point after the command, execute your function in debug mode and do a Quick Watch… on the configuration variable (right click the variable name to select Quick Watch…).

The result of this dive into the code execution is the following list of providers.

Microsoft.Extensions.Configuration.ChainedConfigurationProvider
MemoryConfigurationProvider
HostJsonFileConfigurationProvider
JsonConfigurationProvider for 'appsettings.json' (Optional)
EnvironmentVariablesConfigurationProvider
MemoryConfigurationProvider

The ChainedConfigurationProvider adds an existing IConfiguration as a source. In the default configuration case, adds the host configuration and setting it as the first source for the app configuration.

The first MemoryConfigurationProvider adds the key/value {[AzureWebJobsConfigurationSection, AzureFunctionsJobHost]}. At least it does this in the Development environment. At the time I am writing this I can find no documentation on the purpose of this MemoryConfigurationProvider or AzureWebJobsConfigurationSection.

The HostJsonFileConfigurationProvider is another one of those under the covers undocumented providers, however in looking at documentation on host.json (https://learn.microsoft.com/en-us/azure/azure-functions/functions-host-json) it appears to be responsible for pulling this metadata.

The JsonConfigurationProvider for appsettings.json appears to be an obvious correlation to ASP.NET Core’s use of appsettings.json except for one thing which is it does not work. After some investigation I found that the Source FileProvider Root was not set to the applications root folder where the file is located but instead some obscure AppData folder (C:\Users%USERNANE%\AppData\Local\AzureFunctionsTools\Releases\3.15.0\cli_x64). Go fish.

The EnvironmentVariablesConfigurationProvider loads the environment variable key-value pairs.

The second MemoryConfigurationProvider adds the key/value {[AzureFunctionsJobHost:logging:console:isEnabled, false]}. At least it does this in the Development environment. Again, at the time I am writing this I can find no documentation on the purpose of this MemoryConfigurationProvider or AzureFunctionsJobHost.

Now the interesting thing that needs to be pointed out is that no where in the configuration is any mention of local.settings.json. That’s because local.settings.json is NOT part of the ConfigurationBuilder process. Instead local.settings.json is part of Azure Functions Core Tools which lets you develop and test your functions on your local computer (https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local). The Azure Function Core Tools only focus on specific sections and key/values like IsEncrypted, the Values and ConnectionString sections, etc. as defined in the documentation (https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local#local-settings-file). What happens to these key/values is also unique. For example, key/values in the Values section are inserted into the environment as variables. Most developers don’t even notice that local.settings.json is by default set to be ignored by Git which also makes it problematic should you remove the repository from you development environment only to restore it in the future. Something that ASP.NET Core has fixed with app secrets (https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets).

So, what happens if we create our own configuration with ConfigurationBuilder as suggested in the original question

IConfiguration config = new ConfigurationBuilder()
   .SetBasePath(context.FunctionAppDirectory)
   .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
   .AddEnvironmentVariables()
   .Build();

or using the example shown in one of the other answers?

ExecutionContextOptions executionContextOptions = builder.Services.BuildServiceProvider().GetService<IOptions<ExecutionContextOptions>>().Value;

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
    .SetBasePath(executionContextOptions.AppDirectory)
    .AddEnvironmentVariables()
    .AddJsonFile("appsettings.json", false)
    .AddJsonFile("local.settings.json", true)
    .AddUserSecrets(Assembly.GetExecutingAssembly(), true);

The following are just a few of the issues with both examples.

  • The second example is incorrectly ordered as AddEnvironmentVariables should come last.

  • Neither of the examples mentions the need for the following line of code. List item

    builder.Services.AddSingleton<IConfiguration>(configurationBuilder.Build());

    Without this line the configuration only exist in the Configure(IFunctionsHostBuilder builder) method of your FunctionStartup class. However, with the line you replace the configuration your Azure Function build under the covers. This is not necessarily a good thing as you have no way of replacing providers like HostJsonFileConfigurationProvider.

  • Reading the local.settings.json file (.AddJsonFile("appsettings.json")) will NOT place the key/value pairs in the Values section into the configuration as individual key/value pairs as expected, but instead group them under the Values section. In other word, if for example you want to access {["AzureWebJobsStorage": ""]} in Values you might use the command configuration.GetValue("Values:AzureWebJobsStorage"). The problem is that Azure is expecting to access it by the key name "AzureWebJobsStorage". Even more interesting is the fact that since local.settings.json was never part of the ConfigurationBuilder process this is redundant as Azure Functions Core Tools has already placed these values into the environment. The only thing this will do is allow you to access sections and key/values not defined as part of local.settings.json (https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local#local-settings-file). But why would you want to pull configuration values out of a file that will not be copied into your production code?

All of this brings us to a better way to affect changes to the configuration without destroying the default configuration build by Azure Function which is to override the ConfigureAppConfiguration method in your FunctionStartup class (https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#customizing-configuration-sources).

The following example takes the one provided in the documentation a step further by adding user secrets.

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

    builder.ConfigurationBuilder
        .SetBasePath(context.ApplicationRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
        .AddJsonFile($"appsettings.{context.EnvironmentName}.json", optional: true, reloadOnChange: false)
        .AddUserSecrets(Assembly.GetExecutingAssembly(), true, true)
        .AddEnvironmentVariables();
}

By default, configuration files such as appsettings.json are not automatically copied to the Azure Function output folder. Be sure to review the documentation (https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#customizing-configuration-sources) for modifications to your .csproj file. Also note that due to the way the method appends the existing providers it is necessary to always end with .AddEnvironmentVariables().

Terence Golla
  • 1,018
  • 1
  • 13
  • 12
  • Thanks for sharing! this answer. Just a side note, this solution requires Microsoft.Azure.Functions.Extensions 1.1.0 (the latest) – J.J Dec 17 '20 at 22:45
  • Have you used Refresh configuration with this approach? It works for me with the old configuration style https://github.com/Azure/AppConfiguration/blob/main/examples/DotNetCore/AzureFunction/FunctionApp/Startup.cs but not with the one you are using as done here https://stackoverflow.com/a/60349484/1918727 – J.J Dec 30 '20 at 22:31
  • 3
    This is the only answer I have been able to find that describes what is really happening between local development (Azure Functions Core Tools) and production environments. – trevorc Feb 16 '21 at 21:47
  • Thank you, Terence. Your explanation is very useful. As the values in local.settings.json are never copied over to the production and are only useable for development machine where we need to store "ScheduleTriggerTime": "0 */5 * * * *" configurations? This value is picked up by the Azure Function automatically when we use TimerTrigger i.e. [TimerTrigger("%ScheduleTriggerTime%")] TimerInfo myTimer. So if we want this different based on our environment which configuration source/file do we need to use? – Rizwan Apr 18 '22 at 16:36
  • Took me 2 days to come to the same conclusion as this post. 3 years later why on earth isn't the default Aure Function project template set up like this out of the box! – Neutrino Jun 07 '23 at 08:44
8

You don't need any Configuration object in Azure Functions (v2). All the app settings get injected as Environment variables. So you can do just a simple Environment.GetEnvironmentVariable()

When running locally the local.settings.json gets read in the same way.

see here: https://learn.microsoft.com/en-us/sandbox/functions-recipes/environment-variables?tabs=csharp

cimnine
  • 3,987
  • 4
  • 33
  • 49
silent
  • 14,494
  • 4
  • 46
  • 86
  • Are you saying that builder.Services.Configure is now redundant? – Paul Michaels Aug 05 '19 at 14:43
  • not for DI in general, but you don't need it to read app settings. – silent Aug 05 '19 at 15:04
  • Just make sure that for `local.settings.json`, your settings go under the `Values` section: – White hawk Apr 17 '20 at 10:45
  • 1
    This is actually only true when not using DI, If you specify a function startup class for some reason the local.settings.json values fail to get injected into the environment variables. Really weird. – JoeTomks Jan 21 '22 at 15:18
1

There's a solid way to do this, answered here: Get root directory of Azure Function App v2

The fact that Function Apps use environment variables as the typical way to get configuration is, while true, not optimal IMO. The ability to acquire an appsettings.json file in addition to items that deserve to be environment variables has its place.

The number of env vars being set via the DevOps task: "Azure App Service Deploy" option "Application and Configuration Settings" > "App settings" gets completely out of hand.

This is my implementation of it at the time of this writing:

ExecutionContextOptions executionContextOptions = builder.Services.BuildServiceProvider().GetService<IOptions<ExecutionContextOptions>>().Value;

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
    .SetBasePath(executionContextOptions.AppDirectory)
    .AddEnvironmentVariables()
    .AddJsonFile("appsettings.json", false)
    .AddJsonFile("local.settings.json", true)
    .AddUserSecrets(Assembly.GetExecutingAssembly(), true);

This allows me to leverage my release process variables to do environment specific "JSON variable substitution" for the bulk of my configuration, which lives in a nicely structured appsettings.json that is set to Copy Always. Notice that the loading of appsettings.json is set to not optional (the false setting), while I have local settings and secrets set to optional to accommodate local development.

appsettings.json can then be formatted nice and structured like this. Release variables named properly, e.g. "MyConfig.Setting" will replace the value properly if you set your release to do JSON variable substitution.

{
  "Environment": "dev",
  "APPINSIGHTS_INSTRUMENTATIONKEY": "<guid>",
  "MyConfig": {
    "Setting": "foo-bar-baz"
  }
}

While local.settings.json remains in the flat style:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=<accountname>;AccountKey=<accountkey>;EndpointSuffix=core.windows.net",
    "Strickland:Propane:Config": "Dammit, Bobby!"
  }
}

In addition, I set some App settings (env vars) to Azure KeyVault References in the release process, as well as the minimum settings that are required for Azure Function runtime to start properly and communicate properly with app insights live metrics.

Hope this helps someone who, like me, hates the ever-growing mass of -Variable.Name "$(ReleaseVariableName)" items in App settings. :)

Jim Speaker
  • 1,303
  • 10
  • 28
0

Unfortunately, at present there is no standard way to get the local running directory. It would be best if ExecutionContext or something similar exposed this.

In the absence of a standard way, I am using AzureWebJobsScriptRoot environment variable to get the current working directory, but it only works locally. In azure environment, I am using Environment.GetEnvironmentVariable("HOME")}\\site\\wwwroot.

I posted a code for this in response to a similar question here: Azure Functions, how to have multiple .json config files

There is also a similar solution at this github issue.

Turbo
  • 2,179
  • 18
  • 38
  • The `ExecutionContext` object is available inside your function if you need it: https://github.com/Azure/azure-functions-host/wiki/Retrieving-information-about-the-currently-running-function – silent Sep 09 '19 at 07:02
  • @silent Is it available when Startup class is executing? – Turbo Sep 09 '19 at 18:16
  • 1
    @Turbo take a look at the answer I provided above. – Jim Speaker Oct 19 '20 at 01:02
0

You might want to use GetContext().ApplicationRootPath, for example:

[assembly:FunctionsStartup(typeof(SampleFunction.FunctionsAppStartup))]

namespace SampleFunction
{
  public class FunctionsAppStartup : FunctionsStartup
  {
    public override void Configure(IFunctionsHostBuilder builder)
    {
      string appRootPath = builder.GetContext().ApplicationRootPath;

      // ...
    }
  }
}
Marek Stój
  • 4,075
  • 6
  • 49
  • 50