9
votes

I currently have an azure function using the ServiceBusTrigger binding

 [ServiceBusTrigger("%TopicName%", "%SubscripionName%", Connection = "MyConnection")]
         string  catclogueEventMsgs, ILogger log, ExecutionContext context)

which uses this local.settings.json file

   "Values": {
             …
    "MyConnection": "Endpoint=sb://testxxxxxxxxxxxxxxxxxx
    "SubscriptionName": "testsubscriptionName"
    "TopicName": "testtopicName",
  }

How do I represent this in the appsettings.json file. Will it be like the below?

   "Values": {
    "MyConnection": "Endpoint=sb://testxxxxxxxxxxxxxxxxxx
    "SubscriptionName": "testsubscriptionName"
    "TopicName": "testtopicName",
  }

Instead of using a “Values” object can I use eg “MySubs” object like the below?

   "MySubs": {
    "MyConnection": "Endpoint=sb://testxxxxxxxxxxxxxxxxxx
    "SubscriptionName": "testsubscriptionName"
    "TopicName": "testtopicName",
  }

If its possible to use the above settings, how do I represent this in the ServiceBusTrigger binding? would i change it to this?

 [ServiceBusTrigger("%MySubs.TopicName%", "%MySubs.SubscripionName%", Connection = "MySubs.MyConnection")]
         string  catclogueEventMsgs, ILogger log, ExecutionContext context)
4

4 Answers

21
votes

You CAN indeed read settings outside the Values array as follows:

WeatherApiConfig.cs

public class WeatherApiConfig
{
    public string WeatherApiUrl { get; set; }
    public string WeatherApiKey { get; set; }
}

New for Azure Functions V2, we have an appropriate way to handle DI as shown below:

Startup.cs

[assembly: FunctionsStartup(typeof(BlazingDemo.Api.Startup))]

namespace BlazingDemo.Api
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            var apiConfig = new WeatherApiConfig();
            config.Bind(nameof(WeatherApiConfig), apiConfig);

            builder.Services.AddSingleton(apiConfig);
            builder.Services.AddHttpClient();
        }
    }
}

Local.settings.json

{  
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "WeatherApiConfig": {
    "WeatherApiUrl": "http://api.openweathermap.org/data/2.5/weather",
    "WeatherApiKey": "**removed**"
  }
}

Note: The key for me was to add .SetBasePath(Directory.GetCurrentDirectory()) in Startup.cs since it couldn't find the file without it.

In production I use the function app's Application Settings section to configure these two properties as follows:

Application Settings in Azure

3
votes

I am afraid it's not possible. It's by design that locally Azure Function read Values in local.settings.json file to retrieve settings related to bindings.

Check the description for Values in local.settings.json.

Collection of application settings and connection strings used when running locally. These values correspond to app settings in your function app in Azure, such as AzureWebJobsStorage and AzureWebJobsDashboard.

Many triggers and bindings have a property that refers to a connection string app setting, such as Connection for the Blob storage trigger. For such properties, you need an application setting defined in the Values array.

For production, i.e. on Azure site, we can only rely on Application settings to retrieve settings related to bindings.

0
votes

It looks like a Hybrid of Jason and Oneil/Liam is correct.

  • From what I can tell, in the name/declaration/definition of the function it has to come from the local.settings.json when developing and referred to like "%TheQueueName%", and then entered in Function Configuration -> Application Settings for Azure.

  • In the actual function itself, you can leverage json file injection via Startup.cs and then include the values in the function itself.

This deserves a totally separate blog post (and there have been similar, yet not quite as sophisticated setups mentioned, but here is what I've done and figured out after a year plus and works fantastically.

Files setup:

  1. I have a Config folder in the Functions project which stores xyz.settings.json files. The files here are set to Content, and then to Build and also Publish in the .csproj file.
  2. In the Config folder I also have a secrets.json file which I store all the secrets I do not want published. This is set to build only, not publish! Each xyz.settings.json file has a value of "--SECRET--" for values that I want secret.
  3. local.settings.json has the value pairs for "Values" which is used for dev. This is also for build only. These values are then added to the Function portal in Azure for the published app. These values are only used for the function name/declaration/definition.
  4. I have created a class that corresponds to the values in the xyz.settings.json file. This allows you the benefit of accessing the values by injection. Also, in the Function Application Settings you can refer to these values by XyzSettings:Value when entering a secret value.

Configuration Setup: Startup.cs

(even though it says /home/site/wwwroot - the files are based off of the root of the project, when published that will be the root)

public override void Configure(IFunctionsHostBuilder builder)
  {
    var currentDirectory = "/home/site/wwwroot";
    var config = new ConfigurationBuilder()
        .SetBasePath(currentDirectory)
        .AddJsonFile("local.settings.json", optional: false, reloadOnChange: true)
        .AddJsonFile("Config/xyz.settings.json", optional: false, reloadOnChange: true)
        .AddJsonFile("Config/xyz.settings.dev.json", optional: true, reloadOnChange: true)
        .AddJsonFile("Config/secret.settings.json", optional: true, reloadOnChange: true)
        .AddEnvironmentVariables()
        .Build();
    builder.Services.Configure<XyzSettings>(config.GetSection("XyzSettings"));

What this does and how it works:

  • Application settings are applied in a very specific order, with the json files applying in the order that they are added. If there are duplicate settings, the existing files are overwritten.
  • I have local.settings first. Then followed by general json files (xyz.settings.json), followed by environmental specific variants (zyx.settings.dev.json), followed by secrets.json
  • Environmental specific variables go last (these are either what you specify on your machine - or the equivalent are the Function Application Settings)
  • This approach allows you to have a really nice and stable set of settings. This is especially useful if you have a project that has many different integrations, apis, etc. They can all be different files.
  • Notice how the last two json files are marked as optional. They have to be since they are not included when published. Any json files that are required and are not included will cause the function to fail.

Speaking of Secrets:

  • Local Secrets can be stored in the secrets.json as long as they are not set to publish.

  • In Azure, it is recommended to store the values in Function App Settings which reach out to Azure Key Vault.

  • It is brilliant how it is configured. All you do in the Function App Settings is to name the variable what it is in settings, like XyzSettings:SettingName and then refer to the location in Key Vault like follows:

    @Microsoft.KeyVault(SecretUri=https://yourvalutname.vault.azure.net/secrets/secret-name/auto-generated-keyvaultguid)

Function File: (using a Queues trigger as an example but operates the same way)

namespace Xyz.Functions
{
   public class Azure_Queue_Add_Xyz
   {
        private readonly XyzSettings _xyzSettings = null;

        public Azure_Queue_Add_Xyz(IOptions<AzureSettings> azure_settings)
        {
            _azureSettings = azure_settings.Value;
        }

        [FunctionName("Azure_Queue_Add_Xyz")]
      public void Run(
            [HttpTrigger(AuthorizationLevel.Function, "post",
            Route = "HttpTrigger/Azure_Queue_Add_Xyz")] xyzConfig input,
            [Queue("%TheQueueName%"), StorageAccount("StorageAccount")] ICollector<string> msgOutput,
            ILogger logger,
            ExecutionContext context)
      {
            logger.LogError("{0} is processing a request", context.FunctionName);
            logger.LogError("{0} - Queue: {1}", context.FunctionName, _xyzSettings.TheQueueName);
            logger.LogError("{0} - CloudQueueMessage: {1}", context.FunctionName, JsonConvert.SerializeObject(input));

            msgOutput.Add(JsonConvert.SerializeObject(input));
      }
   }
}
0
votes

This is how to do it for .NET 5, example with some overriding of secrets as well:

KotobeeSettings:

public class KotobeeSettings
{
    public string BaseUrl { get; set; }

    public string SerialNumber { get; set; }
}

Program.cs:

public class Program
{
    public static void Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .ConfigureAppConfiguration((hostContext, builder) =>
            {
                builder.AddJsonFile("local.settings.json");
                //This will override any values added to local.settings.json if you wish to check in this file - Recommended approach for keeping secrets in dev is this though:
                //https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&tabs=windows#register-the-user-secrets-configuration-source
                //builder.AddJsonFile("secrets.settings.json");

                if (hostContext.HostingEnvironment.IsDevelopment())
                {
                    builder.AddUserSecrets<Program>();
                }

            })
            .ConfigureServices((hostContext, services) =>
            {
                var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings:DefaultConnection");
                services.AddDbContext<ApplicationDbContext>(options =>
                        options.UseSqlServer(
                        connectionString,
                        sqlServerOptions => sqlServerOptions.CommandTimeout(600)));

                services.AddHttpClient();

                var configuration = hostContext.Configuration;

                var settings = new KotobeeSettings();
                configuration.Bind("KotobeeSettings", settings);
                services.AddSingleton(settings);

                services.AddTransient<KotobeeClient>();
                services.AddTransient<OrderRowService>();
            })
            .Build();
        CreateDbAndApplyMigrations(host);

        host.Run();
    }

    private static void CreateDbAndApplyMigrations(IHost host)
    {
        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            var context = services.GetRequiredService<ApplicationDbContext>();
            context.Database.Migrate();
        }
    }

Example local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Project.Ebook;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "KotobeeSettings": {
    "BaseUrl": "BaseURL",
    "SerialNumber": "--SECRET--"
  }
}

Example secrets.settings.json and secrets.json:

{
  "KotobeeSettings": {
    "SerialNumber": "123"
  }
}

Settings injected like this:

public class TimerTrigger
{
    private readonly OrderRowService _orderRowService;
    private readonly KotobeeSettings _settings;

    public TimerTrigger(OrderRowService orderRowService, KotobeeSettings settings)
    {
        _orderRowService = orderRowService;
        _settings = settings;
    }

    [Function("TimerTrigger")]
    public async Task RunAsync([TimerTrigger("0 */1 * * * *")] MyInfo myTimer, FunctionContext context)
    {
        var baseUrl = _settings.BaseUrl;

        var logger = context.GetLogger("TimerTrigger");
        logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus?.Next}");
    }
}

public class MyInfo
{
    public MyScheduleStatus ScheduleStatus { get; set; }

    public bool IsPastDue { get; set; }
}

public class MyScheduleStatus
{
    public DateTime Last { get; set; }

    public DateTime Next { get; set; }

    public DateTime LastUpdated { get; set; }
}