14

I was trying to call GetSection from injected configuration in the Startup.cs. The Value was null, while indexer to a concrete section value returns non-null value. It seems to me a bug behind the GetSection method or I am wrong with it?

appsettings.json:

{ "MyConfig": { "ConfigA": "valueA", "ConfigB": "valueB" } }

Program.cs:

    public static void Main(string[] args)
    {
        var host = BuildWebHost(args);
        host.Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();

Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        var mySection = this.Configuration.GetSection("MyConfig");

        var myVal = this.Configuration["MyConfig:ConfigA"];

3 Answers 3

12

I first checked to see if there were any changes between 1.1.1 and 2.x of JsonConfigurationProvider.cs, the internal code that ultimately constructs retrievable values from your JSON file. There were no changes to this or any of the other code that ultimately gets called by your this.Configuration.GetSection("MyConfig");.

The way retrieving values works is that the Configuration will look for your key MyConfig in each config provider in reverse order as defined in code until a value is found. In your example, providers (json, envionment variables, command line args) are provided within Webhost.CreateDefaultBuilder() (see here).

Looking at the code for JsonConfigurationFileParser.cs, it builds a Dictionary<string, string> for keys and values, but only for primitive values. That is, no key is stored for MyConfig (at this level it is an object), but there will be a key for MyConfig:ConfigA, and array values would look like MyConfig:ConfigA:0, MyConfig:ConfigA:1, etc.

Lastly, you will find that Configuration.GetSection("MyConfig") always returns you a newly constructed ConfigurationSection that is never null, and at the very worst will have a Value property of null.

So what happens when you hover over a ConfigurationSection with Intellisense and look at the Value property is that every config provider had been searched and none was found to have a key of "MyConfig" with a primitive value converted to string to return.

You will at the very least need to call:

services.Configure<MyConfigOptions>(configuration.GetSection("MyConfig"));
services.AddSingleton(cfg => cfg.GetService<IOptions<MyConfigOptions>>().Value);

to have it injected throughout your app as a C# object. Otherwise, call individual values with the colon ["MyConfig:ConfigA"] separator syntax or with var mySection = this.Configuration.GetSection("MyConfig")["ConfigA"];, which is redundant but illustrates it is only used to retrieve primitives.

To bind to C# objects and inject them, I created the following extension method:

public static class IServiceCollectionExtensions
{
    public static IServiceCollection AddConfigOptions<TOptions>(this IServiceCollection services,
        IConfiguration configuration, string section) where TOptions : class, new()
    {
        services.Configure<TOptions>(configuration.GetSection(section));
        services.AddSingleton(cfg => cfg.GetService<IOptions<TOptions>>().Value);
        return services;
    }
}

which can be called like this:

public class Startup
{
    public Startup(IConfiguration configuration) => Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddConfigOptions<EmailOptions>(Configuration, "Email")

and injected like this:

public class EmailSender : IEmailSender
{
    private EmailOptions _emailOptions;

    public EmailSender(EmailOptions options) => _emailOptions = options;
Sign up to request clarification or add additional context in comments.

2 Comments

This works and in no way is this clear from the MS documentation.
That's because this goes against what Microsoft says to do. You should inject IOptions into your classes rather than trying to inject your configuration instance.
1

In my case, I was missing a package:

Microsoft.Extensions.Configuration.Binder

It's in fact documented as a subtle code comment in here

Comments

0

I found AspNetCore 2.0 to be so much simpler than 1.x in terms of configuration.

If you put a breakpoint in the Startup(IConfiguration configuration) constructor, you'll notice that the configuration variable has about 5 providers.

The JsonConfigurationProvider is the provider you're interested in for the appsettings.json file, but you will probably notice that the Data property has a Count=0. That is most likely due to the fact that your application is looking for the appsettings.json file in the directory specified in the JsonConfigurationProvider.Source.FileProvider.Root (which is by default wwwroot).

All I did was add an MSBuild task in the .csproj file as follows:

<Copy SourceFiles="appsettings.json" DestinationFolder="wwwroot" />

And that seemed to work perfectly.

This is obviously useful during local development only. However, the general practice these days is never to have configuration in a file in the first place, especially since it will end up being part of your repo history. Instead, when deploying your app, the two common practices these days are using environment variables to override your values, or even better using a key/value store like consul.

Moreover, I see a whole bunch of fancy examples online on how to use the services.Configure<>() which is fine, but the Startup.cs already uses DI to inject values to IConfiguration configuration. That means that IConfiguration is already registered in .NET core's IoC Container, so that essentially means you can already use IConfiguration in any other class in your app like so:

public JobsRepository(IConfiguration configuration)
{
    this.configA = configuration.GetSection("MyConfig:ConfigA").Value;
}

3 Comments

You DO NOT ever want to copy appsettings.json to wwwroot. if you do so, you are making it publicly available(viewable). Why would you ever want to expose your configuration like that?
During startup it looks for appSettings.json in your WebRoot. This is obviously useful during local development only. However, the general practice these days is never to have configuration in a file in the first place, especially since it will end up being part of your repo history. Instead, when deploying your app, the two common practices these days are using environment variables to override your values, or even better using a key/value store like consul... There is no need for negative voting without clarification first.
Even for local development, it should never go there. The file belongs in your /bin compiled output directory next to your other binaries. Aspnetcore will load it from there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.