.net.net-coreconfigurationasp.net-core-hosted-servicesihostedservice

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


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?


Solution

  • 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.