How to map environment variables to a config object in a IHostedService?

3k views Asked by At

I'm creating a new console app for the first time in a while and I'm learning how to use IHostedService. If I want to have values from appsettings.json available to my application, the correct way now seems to be to do this:

public static async Task Main(string[] args)
{
    await Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<MyHostedService>();

                    services.Configure<MySettings(hostContext.Configuration.GetSection("MySettings"));
                    services.AddSingleton<MySettings>(container =>
                    {
                        return container.GetService<IOptions<MySettings>>().Value;
                    });
                })
                .RunConsoleAsync();
}

public class MyHostedService
{
    public MyHostedService(MySettings settings)
    {
        // values from MySettings should be available here
    }
}

public class MySettings
{
    public string ASetting {get; set;}
    public string AnotherSetting {get; set; }
}

// appsettings.json
{  
    "MySettings": {
        "ASetting": "a setting value",
        "AnotherSetting":  "another value"
  }
}

And that works and it's fine. However, what if I want to get my variables not from an appsettings.json section but from environment variables? I can see that they're available in hostContext.Configuration and I can get individual values with Configuration.GetValue. But I need them in MyHostedService.

I've tried creating them locally (i.e. as a user variable in Windows) with the double-underscore format, i.e. MySettings_ASetting but they don't seem to be available or to override the appsettings.json value.

I guess this means mapping them to an object like MySettings and passing it by DI in the same way but I'm not sure how to do this, whether there's an equivalent to GetSection or whether I need to name my variables differently to have them picked up?

3

There are 3 answers

5
ArwynFr On BEST ANSWER

Edit 1:

This code is problematic:

services.AddSingleton<MySettings>(container =>
{
  return container.GetService<IOptions<MySettings>>().Value;
});

Calling GetService will build the service provider but then you try to add a singleton to the service provider. This will not work. You should inject the IOptions<MySettings> in the service rather.


Edit 2:

In my experience double underscore does not work well, if you can, prefer to use a colon separated key, such as MySettings:AnotherValue.


A more modern answer:

MySettings.cs

public class MySettings
{
    // Add default configuration path so it can be reused elsewhere
    public const string DefaultSectionName = "MySettings";

    public string AnotherSetting { get; set; } = string.Empty;

    public string ASetting { get; set; } = string.Empty;
}

MyHostedService.cs

public class MyHostedService : IHostedService
{
    private readonly MySettings settings;

    // IOptions<TOptions> or IOptionsMonitor<TOptions>
    // these interfaces add useful extensions features
    public MyHostedService(IOptions<MySettings> settings)
    {
        this.settings = settings.Value;
    }

    // IHostedService Implementation redacted
}

Progam.cs

// Use top-level statements, linear and fluent service declaration:
var builder = Host.CreateApplicationBuilder(args);
builder.Services

    // Declare MyHostedService as a HostedService in the DI engine
    .AddHostedService<MyHostedService>()

    // Declare IOption<MySettings> (and variants) in the DI engine
    .AddOptions<MySettings>()

    // Bind the options to the "MySettings" section of the config
    .BindConfiguration(MySettings.DefaultSectionName);

await builder.Build().RunAsync();

Read settings from CLI or environment

Host builders support loading the configuration from the environment variables by default:

Properties/launchSettings.json

{
  "profiles": {
    "run": {
      "commandName": "Project",
      "environmentVariables": {
        "MySettings:AnotherSetting": "test-another"
      }
    }
  }
}

Unlike Host.CreateDefaultBuilder, Host.CreateApplicationBuilder supports loading the configuration from the CLI as well. You can use dotnet run --MySettings:AnotherSetting test to override the contents of the appsettings.json file. Please note that setting the variable on the CLI overrides the environment variable values.

4
pfx On

In case you have your environment variables declared as ASetting and AnotherSetting, then in ConfigureServices you'll need to add a bind to the full IConfiguration holding the environment variables, instead of only to one with a MySettings section name/path, since this name/path is also taken into account for finding the corresponding environment variables - see the alternative approach further below.

Below extension methods are from Microsoft.Extensions.DependencyInjection version 7.0.0 which runs on .NET 6, see the documentation.

services.AddOptions<MySettings>()
    .BindConfiguration("MySettings") // Binds to the appsettings section.
    .Bind(hostContext.Configuration); // Binds to e.g. the environment variables.

Full code:

await Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<MyHostedService>();
        
        services.AddOptions<MySettings>()
            .BindConfiguration("MySettings")
            .Bind(hostContext.Configuration);

        services.AddSingleton<MySettings>(container =>
        {
            return container.GetService<IOptions<MySettings>>().Value;
        });
    })
    .RunConsoleAsync();

Alternatively, you can declare these environment variables as MySettings:AnotherSetting and MySettings:AnotherSetting, in that case it suffices to make one of below calls.

services.AddOptions<MySettings>().BindConfiguration("MySettings");

Or the code you already had, without Bind(hostContext.Configuration).

services.Configure<MySettings>(hostContext.Configuration.GetSection("MySettings"));
4
Serge On

if you want to get the variables not from an appsettings.json section but from environment variables you can add a ConfigureAppConfiguration part or/and ConfigureHostConfiguration to Host

  await Host
    .CreateDefaultBuilder(args)
    .ConfigureHostConfiguration((config) =>
    {
        config.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("hostsettings.json", optional: true)
        .AddEnvironmentVariables(prefix: "PREFIX_");
    })
    .ConfigureAppConfiguration((hostContext, configBuilder) =>
    {
     configBuilder
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json") 
    // or 
    .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", 
                                                  optional: true, reloadOnChange: true) 
     //or just any name explicitly
    .AddJsonFile(@"C:\...\mySettings.json")
    .Build();
    })
    .ConfigureServices((hostContext, services) =>
    {
       services.Configure<MySettings>(hostContext.Configuration.GetSection("MySettings"));
       services.AddHostedService<MyHostedService>();
    })
    .RunConsoleAsync();

and service

public class MyHostedService : IHostedService
{
        private readonly MySettings _settings;

    public MyHostedService(IOptions<MySettings> settings)
    {
        _settings = settings.Value;
    }
    public Task StartAsync(CancellationToken cancellationToken) {};

    public Task StopAsync(CancellationToken cancellationToken) {};
}