Skip to content

When using a FunctionsStartup class, settings are not read from the correct directory #4517

@logiclrd

Description

@logiclrd

In creating a simple Azure Function using D.I. (a FunctionsStartup class) and ASP.NET Core 2 style configuration, I have run into issues with it not reading configuration from the correct directory.

In my Startup.Configure method, I tried writing code like this:

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

However, all of the configuration properties are always null. Drilling into it a bit with the debugger, it looks like the registered IConfiguration does in fact have a JsonConfigurationProvider, but it is watching the wrong directory. It looks like the JsonConfigurationProvider assumes that AppContext.BaseDirectory will be the bin directory of the web app, but when the Functions app is starting up, AppContext.BaseDirectory points at the directory containing the Functions framework bits (e.g. C:\Users\Jonathan Gilbert\AppData\Local\AzureFunctionsTools\Releases\2.22.0\cli\).

I have not found a non-ugly work-around for this yet.

Repro steps

  • Create a new Azure Functions project, HTTP trigger.
  • Create a class AppSettings.cs with a dummy setting in it:
    public class AppSettings
    {
      public string DummySetting { get; set; }
    }
  • Create a file appsettings.json with a value for this setting:
    {
      "AppSettings":
      {
        "DummySetting": "foobar"
      }
    }
  • Configure appsettings.json to be copied to the build output if newer.
  • Add a NuGet reference to Microsoft.Azure.Functions.Extensions. (At the time of writing, only version 1.0.0 is published.)
  • Create a class Startup.cs with code to register IOptions<AppSettings> via the configuration subsystem:
    using Microsoft.Azure.Functions.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;

    [assembly: FunctionsStartup(typeof(Startup))]
    public class Startup : FunctionsStartup
    {
      public override void Configure(IFunctionsHostBuilder builder)
      {
        builder.Services.AddOptions<AppSettings>().Configure<IConfiguration>((settings, configuration) => configuration.GetSection("AppSettings").Bind(settings));
      }
    }
  • Create a simple function to consume the settings:
    using Microsoft.AspNetCore.Http;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.Http;

    public class TestFunction
    {
      AppSettings _settings;

      public TestFunction(IOptions<AppSettings> settings)
      {
        _settings = settings.Value;
      }

      [FunctionName("test")]
      public string Test([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
      {
        return _settings.DummySetting;
      }
    }
  • Run the function host locally and call its /api/test endpoint.

Expected behavior

The function should return the "foobar" value assigned to DummySetting in appsettings.json.

Actual behavior

When the function is called, _settings.DummySetting is null.

Place a breakpoint on the configuration callback in the Configure method, and observe that configuration has no JSON configuration loaded, and that its FileProvider is looking at the wrong directory.

image

Known workarounds

The closest thing I have found to a work-around is to obtain a reference to the ScriptApplicationHostOptions object used to initialize the parent ScriptHost. I haven't located a "proper" way to do this, so my "workaround" involves using reflection to extract the value of a private member (in this case, the _hostOptions member of the HostJsonFileConfigurationProvider configuration provider). This then provides a ScriptPath value that can be used to override the FileProvider for a ConfigurationBuilder and thereby explicitly load the correct appsettings.json file. I then use this IConfiguration object instead of the registered one when resolving IOptions<AppSettings>:

    var appSettingsConfig = new ConfigurationBuilder()
      .SetFileProvider(new PhysicalFileProvider(scriptApplicationHostOptions.ScriptPath))
      .AddJsonFile("appsettings.json")
      .Build();
    
    builder.Services.AddOptions<SlackSettings>().Configure<IConfiguration>(
      (settings, configuration) => appSettingsConfig.GetSection("AppSettings").Bind(settings));
    //                             ^^^^^^^^^^^^^^^^^ NB: I am ignoring the `configuration` supplied by the callback.

This is an extremely poor "workaround", owing to the dependency on a private member.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions