-1

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?

Bob Tway
  • 9,301
  • 17
  • 80
  • 162

3 Answers3

1

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"));
pfx
  • 20,323
  • 43
  • 37
  • 57
  • Microsoft is moving away from `Host.CreateDefaultBuilder()` in favor of `Host.CreateApplicationBuilder()`. See https://stackoverflow.com/a/75392616/6092856 – ArwynFr Apr 11 '23 at 22:20
  • @ArwynFr Thanks for this info; do not that that doesn't change how the binding occurs and that your post is using the same (modern) .NET 7 `BindConfiguration` I'm referring to. `Host.CreateDefaultBuilder` also loads the envionment variables; OP mentions in his question that the environment variables are available in the configuration. – pfx Apr 12 '23 at 06:55
  • See my answer, you are doing the same mistake as OP ; you can 't access the ServiceProvider while configuring it. – ArwynFr Apr 12 '23 at 08:50
  • @ArwynFr That line sets up a callback via a lambda expression, which gets called afterwards; there's no wrong in that. Injecting IOptions is indeed more common, but not a must. – pfx Apr 12 '23 at 08:55
0

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) {};
}
Serge
  • 40,935
  • 4
  • 18
  • 45
  • `CreateDefaultBuilder(args)` already loads IConfiguration from appsettings.json and appsettings.{Environment}.json, and it already sets the content root path, among others, as stated here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder?view=dotnet-plat-ext-7.0 – ArwynFr Apr 11 '23 at 21:41
  • Maybe if you use one settings file in location by default. But what about with different name or /and different locations? – Serge Apr 11 '23 at 22:07
  • If you want to load different files and path, then you need additional statements. But re-adding the default path and files is useless. – ArwynFr Apr 11 '23 at 22:17
  • @ArwynFr For example, I prefer to have config files in special folder, instead of trying to figure where is my application folder, for debugging it can be a temp directory 10 levels deep – Serge Apr 11 '23 at 22:23
0

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.

ArwynFr
  • 1,383
  • 7
  • 13
  • Thanks for this - how do you add support for more than one IOptions in the builder? It won't accept more than one all to AddOptions – Bob Tway Apr 12 '23 at 08:04
  • 1
    `AddOptions()` returns a `OptionsBuilderConfiguration`, if you want to chain them you simply need to revert to the `IServiceCollection` which is avilable in the `Services` proeprty, like this: `builder.Services.AddOptions().Services.AddOptions();` – ArwynFr Apr 12 '23 at 08:38
  • @BobTway i've also added details on why your code does not work well – ArwynFr Apr 12 '23 at 08:45
  • Also note that the `.BindConfiguration()` refers to the `OptionsBuilderConfiguration` of the previous `AddOptions()`, so if you want to chain them and bind them you need to do this in order: `builder.Services.AddOptions().BindConfiguration(X.Path).Services.AddOptions().BindConfiguration(Y.Path);` – ArwynFr Apr 12 '23 at 08:47
  • Yes, I presumed that was the case, but thanks for the clarification. – Bob Tway Apr 12 '23 at 08:49