16

The new Azure Function 3.0 SDK provides a way to implement a Startup class. It gives access to the collection of services that are available by dependency injection, where I can add my own components and third-party services.

But I don't know how to use a configuration file.

[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]
namespace MyNamespace
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
...

My third party services take large structures as parameter, and those configuration files are copied with binaries. I can copy them in a subsection of an appsettings.json file:

{
  "MachineLearningConfig" : {
     ( about 50+ parameters and subsections )
  }
}

Configuration values are updated according to the environment of deployment . I use Azure Devops's File Transform Task for that: production values are different from staging and dev values.

Given the documentation https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection the way to load those options is:

builder.Services.AddOptions<MachineLearningConfig>()
                .Configure<IConfiguration>((settings, configuration) =>
                                           {
                                                configuration.GetSection("MachineLearningConfig").Bind(settings);
                                           });

But that requires to add all settings as key/value strings in the host's environment, and that is what I do not want to do. There are too many of them and that is not as easy to maintain as in a json configuration file.

I copied that appsettings.json alongside the host.json.

But the appsettings.json file read at startup by the Azure Function SDK is not my application's appsettings.json but Azure Function tools's appsettings.json. So configuration.GetSection("MachineLearningConfig") returns empty values as there is no appsettings.json file in the Azure Function tools bin folder.

So, my question: how to have my MachineLearningConfig section read from my appsetting.json file injected as IOption<MachineLearningConfig> in my app ?

Anthony Brenelière
  • 60,646
  • 14
  • 46
  • 58
  • Have you considered creating a custom configuration provider https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.2#custom-configuration-provider – Nkosi Feb 03 '20 at 11:54
  • Some clarification is needed on where those 3rd party configurations are located. – Nkosi Feb 03 '20 at 12:09
  • Nkosi, like any asp.net core service, the appsetting.json file is joined at the root in the deployed package. At compile time for debugging it is copied in the bin/Debug directory. – Anthony Brenelière Feb 03 '20 at 13:52
  • Then in that case you will need to create a new configuration. load the original configuration into it and then add the other settings. build that configuration and replace the root config in the DI container. – Nkosi Feb 03 '20 at 14:13

7 Answers7

14

In Azure Functions v3 you can use the appsettings.json configuration pattern from ASP.NET-Core with the ConfigureAppConfiguration call below (reference).

Additionally, change the way you add your options by using the code within the Configure method below. You should not be passing IConfiguration to IServiceProvider.Configure<>(). This will allow you to use an injected IOptions<MachineLearningConfig> object.

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

[assembly: FunctionsStartup(typeof(Startup))]

namespace MyAzureFunction
{
    public class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            if (builder == null) throw new ArgumentNullException(nameof(builder));

            var context = builder.GetContext();

            builder.ConfigurationBuilder
                .AddAppsettingsFile(context)
                .AddAppsettingsFile(context, useEnvironment: true)
                .AddEnvironmentVariables();
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            if (builder == null) throw new ArgumentNullException(nameof(builder));

            var configuration = builder.GetContext().Configuration;

            builder.Services.Configure<MachineLearningConfig>(options =>
            {
                configuration.GetSection("MachineLearningConfig").bind(options);
            });
        }
    }

    public static class ConfigurationBuilderExtensions
    {
        public static IConfigurationBuilder AddAppsettingsFile(
            this IConfigurationBuilder configurationBuilder,
            FunctionsHostBuilderContext context,
            bool useEnvironment = false
        )
        {
            if (context == null) throw new ArgumentNullException(nameof(context));

            var environmentSection = string.Empty;

            if (useEnvironment)
            {
                environmentSection = $".{context.EnvironmentName}";
            }

            configurationBuilder.AddJsonFile(
                path: Path.Combine(context.ApplicationRootPath, $"appsettings{environmentSection}.json"),
                optional: true,
                reloadOnChange: false);

            return configurationBuilder;
        }
    }
}
Daniel
  • 8,655
  • 5
  • 60
  • 87
  • Do you know how this works with dotnet 5 + functions v3? As far as I'm aware this technique of inheriting from `FunctionsStartup` has been deprecated – NickL Sep 01 '21 at 12:26
  • Sorry @NickL I don't know. I'm hoping they implemented `appsettings..json` out of the box though (or something similar), but I haven't had the chance to look into dotnet 5 yet. – Daniel Sep 01 '21 at 19:50
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:28
  • @YonatanNir There are a few use-cases for this. For example, 1) it may be easier to generate this environment specific configuration file as part of the pipeline instead of setting environment variables. 2) if you need to dynamically update settings on an environment which is already deployed (I wouldn't personally do this one but have seen it in the past) you'd just need to update the file. I don't understand what you mean by config var? The config var is an env var. – Daniel Apr 08 '22 at 21:56
  • @YonatanNir Additionally, If you have a lot of variables and some don't change very often, it makes sense to store a lot of them via these files. With the above code any setting from the file can be overwritten by an env var as well. So you can store many of the settings in some of these files, using environment variables to override some of the more dynamic ones. Can this be done by adding the file to the sln? Not when I originally wrote this answer. But don't confuse this with ASP.NET-Core which had support for these files out of the box. – Daniel Apr 08 '22 at 22:01
  • @Daniel I think using appsettings{environmentSection}.json file is not a good practice, because appsettings.development.json may be delivered to the production environment, and that file should be theorically ignored. 'Therocally' is an issue. Infra team do not want dev config files to be depositted on production servers, for security reason. Just use a single appsettings.json and update it in your Devops CD process. – Anthony Brenelière Apr 09 '22 at 02:51
  • @AnthonyBrenelière I would agree it's a bad practice to put connection strings and sensitive information in these configuration files. Only simple configuration differences between environments should be put in these files. I wouldn't even inject the sensitive variables in the production file; instead I would pass these down as environment variables which are consumed via the `.AddEnvironmentVariables();` call in the code above. Having this setup makes the variables cleaner and easier to maintain in my opinion. – Daniel Apr 09 '22 at 15:56
8

Nkosi's solution works pretty well, but it does update the way the azure function runtime loads settings for itself, by replacing IConfiguration singleton: services.AddSingleton<IConfiguration>.

I prefer having another IConfigurationRoot that is not injected. I just need to inject my settings IOption<MachineLearningSettings> that are linked to my own IConfigurationRoot.

I build another IConfigurationRoot that is member of the Startup class:

public class Startup : FunctionsStartup
{
    private IConfigurationRoot _functionConfig = null;

    private IConfigurationRoot FunctionConfig( string appDir ) => 
        _functionConfig ??= new ConfigurationBuilder()
            .AddJsonFile(Path.Combine(appDir, "appsettings.json"), optional: true, reloadOnChange: true)
            .Build();

    public override void Configure(IFunctionsHostBuilder builder)
    {
         builder.Services.AddOptions<MachineLearningSettings>()
             .Configure<IOptions<ExecutionContextOptions>>((mlSettings, exeContext) =>
                 FunctionConfig(exeContext.Value.AppDirectory).GetSection("MachineLearningSettings").Bind(mlSettings) );
    }
}

Note: connection strings must remain in the application settings, because it is required by triggers to create an instance of the the function app that is not started (in a consumption service plan).

Francois Botha
  • 4,520
  • 1
  • 34
  • 46
Anthony Brenelière
  • 60,646
  • 14
  • 46
  • 58
  • 1
    The `ExecutionContextOptions` is the key here in finding the correct location to load the settings. Yes this approach is less invasive that my suggested approach and less likely to break default configuration. Good going. Happy coding!!!. – Nkosi Feb 06 '20 at 11:06
  • Do note that although the original settings `IConfiguration` is replaced, it was merged into the replacement so that nothing is lost or overridden. – Nkosi Feb 06 '20 at 11:47
  • Yes, otherwise trigger's connectionstrings in environment would not be taken into account and it would'nt work. – Anthony Brenelière Feb 07 '20 at 23:03
  • @AnthonyBrenelière Is there a way to merge default configuration with ours, while preserving the Azure runtime loads? – user33276346 Jul 23 '20 at 18:50
  • There is a syntax error with `Path.Combine`. There should by an ending parenthesis after `"appsettings.json"` to close that method. – Justin Skiles Aug 11 '20 at 14:10
  • Why not use the ConfigureAppConfiguration method? it is there for a reason – JSON Mar 24 '21 at 16:00
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:27
  • 1
    Because you may use libraries that require options that are more complex then what allows the key/string model of environment variables. – Anthony Brenelière Apr 09 '22 at 02:41
5

With this .NET Core 3.1 and Azure Function 3. Spent a hours days. Here is what I came up with.

[assembly: FunctionsStartup(typeof(Ugly.AzureFunctions.Startup))]

namespace Ugly.AzureFunctions
{
    class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            try
            {
                // On Azure, we need to get where the app is.
                // If you use Directory.GetCurrentDirectory(), you will get something like D:\Program Files (x86)\SiteExtensions\Functions\3.0.14785\32bit
                var basePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..");
                var environmentName = builder.GetContext().EnvironmentName;
                builder.ConfigurationBuilder
                    .SetBasePath(basePath)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true)
                    .AddEnvironmentVariables();
            }
            catch (Exception ex)
            {
                // Handle exceptions about this. Which __should__ never ever happen.
                // The previous comment is sarcastic.
                throw;
            }
        }

        public override void Configure(IFunctionsHostBuilder builder)
        {
            try
            {
                // DO NOT add the configuration as Singleton.
                // If you need the IConfiguration:
                //var configuration = builder.GetContext().Configuration;

                builder.Services
                    .AddOptions<MachineLearningConfig>()
                    .Configure<IConfiguration>((settings, configuration) => {
                        configuration.GetSection("MachineLearningConfig").Bind(settings);
                });
            }
            catch (Exception ex)
            {
                // Handle or not handle? That's the question.
                throw;
            }
        }
    }
}
Daniel
  • 8,655
  • 5
  • 60
  • 87
jsgoupil
  • 3,788
  • 3
  • 38
  • 53
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:28
4

In the startup class:

    IConfigurationRoot config = new ConfigurationBuilder()
              .SetBasePath(Environment.CurrentDirectory)
              .AddJsonFile("someSettings.json", optional: true, reloadOnChange: true)
              .AddEnvironmentVariables()
              .Build();

Add a json file to you project that holds the settings. Note that local.settings.json is ignored/removed during deployment. (Name the file something else.)

Zsolt Bendes
  • 2,219
  • 12
  • 18
  • The current directory does not contain the someSettings.json file at startup, as it points to the azure core tools bin directory. – Anthony Brenelière Feb 02 '20 at 23:02
  • you have to create the file you self – Zsolt Bendes Feb 03 '20 at 07:41
  • 1
    Let me try to simplify the comment above. Create a copy of ```local.settings.json``` at the root and name the file ```someSettings.json```. – Zsolt Bendes Feb 03 '20 at 07:53
  • is this working also with local settings json on local machine? – zolty13 Jul 06 '21 at 11:39
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:25
4

MS Docs has been updated with configuration samples

Remember to install required libraries listed in Prerequisites seciton.

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

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

namespace MyNamespace
{
    public class Startup : FunctionsStartup
    {
        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();
        }
    }
}
pizycki
  • 1,249
  • 4
  • 14
  • 26
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:28
  • @YonatanNir If you have your configurations as plain key-value pairs, then what you suggested would be the easiest way to do it. But sometimes you have to load configurations which have a complex structure with nested sections or worse arrays. Then, loading it off a JSON into a typed object makes more sense. This let's you do that. – Ε Г И І И О Jun 29 '22 at 14:36
  • @ΕГИІИО But you can also save the values to the configurations as a JSON string – CodeMonkey Jun 29 '22 at 17:43
  • @YonatanNir Yeah, it's a matter of preference I guess. It's not intuitive to edit a value inside an escaped JSON string, but then you get the advantage of having everything in one place :) – Ε Г И І И О Jun 30 '22 at 05:31
2

After some research, I came across this thread on Githib

using appsettings.json + IConfiguration in Function App

From which I crafted the following extension based on the comments and suggestions that showed to have worked.

public static class FunctionHostBuilderExtensions {
    /// <summary>
    /// Set up the configuration for the builder itself. This replaces the 
    /// currently registered configuration with additional custom configuration.
    /// This can be called multiple times and the results will be additive.
    /// </summary>
    public static IFunctionsHostBuilder ConfigureHostConfiguration (
        this IFunctionsHostBuilder builder, 
        Action<IServiceProvider, IConfigurationBuilder> configureDelegate) {
        IServiceCollection services = builder.Services;            
        var providers = new List<IConfigurationProvider>();            
        //Cache all current configuration provider
        foreach (var descriptor in services.Where(d => d.ServiceType == typeof(IConfiguration)).ToList()) {
            var existingConfiguration = descriptor.ImplementationInstance as IConfigurationRoot;
            if (existingConfiguration is null) {
                continue;
            }
            providers.AddRange(existingConfiguration.Providers);
            services.Remove(descriptor);
        }
        //add new configuration based on original and newly added configuration
        services.AddSingleton<IConfiguration>(sp => {
            var configurationBuilder = new ConfigurationBuilder();                    
            //call custom configuration
            configureDelegate?.Invoke(sp, configurationBuilder);                
            providers.AddRange(configurationBuilder.Build().Providers);                
            return new ConfigurationRoot(providers);
        });            
        return builder;
    }
}

The main idea is to extract all the currently registered configuration related types, create a new builder, apply custom configuration and build a new configuration with the original and custom configuration details merged into one.

It would then be used in Startup

public class Startup : FunctionsStartup {
    public override void Configure(IFunctionsHostBuilder builder) {
        builder.ConfigureHostConfiguration((sp, config) => {
            var executioncontextoptions = sp.GetService<IOptions<ExecutionContextOptions>>().Value;
            var currentDirectory = executioncontextoptions.AppDirectory;

            config
                .SetBasePath(currentDirectory)
                .AddJsonFile("appSettings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();

            //if there are multiple settings files, consider extracting the list,
            //enumerating it and adding them to the configuration builder.
        });

        builder.Services
            .AddOptions<MachineLearningConfig>()
            .Configure<IConfiguration>((settings, configuration) => {
                configuration.GetSection("MachineLearningConfig").Bind(settings);
            });
    }
}

The above should now be able to get the settings from your custom configuration.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • That is the way. The issue with that solution is that it replaces the azure function runtime 's configuration with services.AddSingleton . In the meantime I found a way that does not require it. – Anthony Brenelière Feb 06 '20 at 09:33
  • This no longer works in Azure Functions v3 (.NET Core 3+) because the `ImplementationInstance` property is `null` for the type `IConfiguration` in `IServiceCollection`. – Justin Skiles Aug 11 '20 at 14:55
  • 4
    It is unacceptable how overly complex this mechanism is. – The Muffin Man Oct 18 '20 at 20:59
0

When you develop a function app locally, you must maintain local copies of these values in the local.settings.json project file. To learn more, see Local settings file.

The easiest way to upload the required settings to your function app in Azure is to use the Manage Application Settings... link that is displayed after you successfully publish your project.

Refer this example on how to get those setting values.

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

here is a sample project

Sajeetharan
  • 216,225
  • 63
  • 350
  • 396
  • As I said I do not want to use application settings (or the environment variables) because they are limited to key/values and not purposed to store large structured settings. I use application settings for some connection strings only. – Anthony Brenelière Feb 02 '20 at 22:56
  • Why should one add this code if one can just add the local file to the sln for local development or keys to the azure func configurations and use System.Environment.GetEnvironmentVariable("keyName") ? Getting an env var sounds much easier and also saves you the trouble of passing this config var everywhere – CodeMonkey Apr 08 '22 at 21:25