8

So I have written an azure function that works locally just fine. I believe this is down to having the local.setting.json file. But when I publish it to azure the function does not work as it can't find the settings values that I had defined. Coming from a web app and console driven approach, we would have different config files that would be associated with each environment. How can I get this to work so I can have multiple settings.json files, e.g. one for dev, stag and prod environments? The end result is to get this deployed with octopus deploy, but at this point, if I cant even get it working with a publish then there is no chance of doing this.

I'm rather confused why this information is not easily available, as would presume it is a common thing to do?

Andrew
  • 2,571
  • 2
  • 31
  • 56
  • One proper way is to add them as ARM template during the deployment of your app so that they appear in your `Function App -> configuration -> Application Settings`. As you have different templates per environment you can vary the variables per template. – NotFound Jun 05 '19 at 08:56

3 Answers3

4

I would love to see functions support environment specific settings in the same way as asp.net core or console apps. In the meantime I am using below code, which is somewhat hacky (see comments).

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        // Get the path to the folder that has appsettings.json and other files.
        // Note that there is a better way to get this path: ExecutionContext.FunctionAppDirectory when running inside a function. But we don't have access to the ExecutionContext here.
        // Functions team should improve this in future. It will hopefully expose FunctionAppDirectory through some other way or env variable.
        string basePath = IsDevelopmentEnvironment() ?
            Environment.GetEnvironmentVariable("AzureWebJobsScriptRoot") :
            $"{Environment.GetEnvironmentVariable("HOME")}\\site\\wwwroot";

        var config = new ConfigurationBuilder()
            .SetBasePath(basePath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)  // common settings go here.
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT")}.json", optional: false, reloadOnChange: false)  // environment specific settings go here
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: false)  // secrets go here. This file is excluded from source control.
            .AddEnvironmentVariables()
            .Build();

        builder.Services.AddSingleton<IConfiguration>(config);
    }

    public bool IsDevelopmentEnvironment()
    {
        return "Development".Equals(Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT"), StringComparison.OrdinalIgnoreCase);
    }
}
Turbo
  • 2,179
  • 18
  • 38
  • 1
    Yes this is similar to what I am doing, but using octopus deploy I will have one json file that will hopefully get the config values injected into, so will put the relevant values for dev, stag and prod. The above approach is what I have, but wanted to make it more compatible with our octopus deploy release. – Andrew Jun 06 '19 at 10:08
  • Building upon this, you can access the `ExecutionContextOptions` like the following: `builder.Services.BuildServiceProvider().GetService>().Value;` – Jamie Rees Feb 10 '20 at 08:39
3

OK so I have it working now :) Because we use octopus deploy we don't want multiple config files so we just have the one appsettings.Release.json file which gets the values substituted base on the environment being deployed too.

Below is the main function code.

public static class Function
    {
        // Format in a CRON Expression e.g. {second} {minute} {hour} {day} {month} {day-of-week}
        // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer
        // [TimerTrigger("0 59 23 * * *") = 11:59pm
        [FunctionName("Function")]
        public static void Run([TimerTrigger("0 59 23 * * *")]TimerInfo myTimer, ILogger log)
        {

            // If running in debug then we dont want to load the appsettings.json file, this has its variables substituted in octopus
            // Running locally will use the local.settings.json file instead
#if DEBUG
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();
#else
            IConfiguration config = Utils.GetSettingsFromReleaseFile();
#endif

            // Initialise dependency injections
            var serviceProvider = Bootstrap.ConfigureServices(log4Net, config);

            var retryCount = Convert.ToInt32(config["RetryCount"]);

            int count = 0;
            while (count < retryCount)
            {
                count++;
                try
                {
                    var business = serviceProvider.GetService<IBusiness>();
                    business.UpdateStatusAndLiability();
                    return;
                }
                catch (Exception e)
                {
                    // Log your error
                }
            }

        }

    }

The Utils.cs file looks as follows

public static class Utils
    {

        public static string LoadSettingsFromFile(string environmentName)
        {
            var executableLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            // We need to go back up one level as the appseetings.Release.json file is not put in the bin directory
            var actualPathToConfig = Path.Combine(executableLocation, $"..\\appsettings.{environmentName}.json");
            using (StreamReader reader = new StreamReader(actualPathToConfig))
            {
                return reader.ReadToEnd();
            }
        }

        public static IConfiguration GetSettingsFromReleaseFile()
        {
            var json = Utils.LoadSettingsFromFile("Release");
            var memoryFileProvider = new InMemoryFileProvider(json);

            var config = new ConfigurationBuilder()
                .AddJsonFile(memoryFileProvider, "appsettings.json", false, false)
                .Build();
            return config;
        }

    }

The appsettings.Release.json is set as Content and Copy Always in visual studio. It looks like this

{
  "RetryCount": "#{WagonStatusAndLiabilityRetryCount}",
  "RetryWaitInSeconds": "#{WagonStatusAndLiabilityRetryWaitInSeconds}",
  "DefaultConnection": "#{YourConnectionString}"
}

Actually I believe you could have an appsettings.config file there already and skip the appsettings.Release.json file, but this is working and you can do what you want with it now.

Andrew
  • 2,571
  • 2
  • 31
  • 56
  • I don't see where you are getting this class:InMemoryFileProvider from. – DotNet Programmer Jun 04 '21 at 20:58
  • I imagine its something like this one https://stackoverflow.com/a/52405277/277067, I don't have the code available. Also googling "ConfigurationBuilder addjsonfile in memory" returns the same page https://stackoverflow.com/questions/44807836/loading-startup-configuration-from-json-located-in-memory – Andrew Jun 06 '21 at 15:10
  • Even with this I am having a hard time getting my individual projects to load the json file I added to the project. I have a settings file that I want the DB project to load it up. dbAppsettings is the file I want to have it load up when I am running the API and have a settings file that is for parsing the Web API objects. – DotNet Programmer Jun 07 '21 at 14:10
1

This doc has the description about local.settings.json:

By default, these settings are not migrated automatically when the project is published to Azure.

One way is using --publish-local-settings:

Publish settings in local.settings.json to Azure, prompting to overwrite if the setting already exists.

Another way is to use the Manage Application Settings, the Remote is the current setting in the function app in Azure. Or choose Add setting to create a new app setting. For details you could refer to this doc:Function app settings.

enter image description here

George Chen
  • 13,703
  • 2
  • 11
  • 26
  • Yes may have something working just need to confirm, Not to keen on publishing a local settings file. also application settings don't seem to give me enough options for all the environments. Ill post my solution soon, cheers – Andrew Jun 06 '19 at 09:41
  • 1
    @Andrew, if you use Azure Functions Core Tools, you could use --publish-local-settings make sure these settings are added to the function app in Azure. But if not, yes you have to add it manually. – George Chen Jun 06 '19 at 09:47