You don't need IConfiguration outside Startup

I'm writing this for those new to ASP.NET Core, based on what I see on StackOverflow. Most of what I've written here is already in the Docs, so check the links at the end of the post for detailed info.

The ASP.NET Core Web Host

Think of the web host as an instance of an application server that processes incoming web requests. We create one (usually in Program.cs) as follows:

public static void Main(string[] args) => 
    CreateHostBuilder(args).Build().Run();

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
            webBuilder.UseStartup<Startup>()
        );

The CreateDefaultBuilder() sets up the app's configuration. Source (v3.0.0). The app's configuration is represented by the IConfiguration interface. (You can also manually create your own WebHostBuilder instance if the defaults don't work for you, in which case you're responsible for setting up the app configuration.)

When the WebHost is built using Build(), your startup class's ConfigureServices() is called. See WebHostBuilder.cs L187, WebHost.cs L110 and WebHost.cs L180.

Using IConfiguration in Startup

You can get an instance of IConfiguration for use in your Startup class by constructor injection:

public class Startup
{
    private IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration) // <-- the framework injects
                                                 // a valid instance
    {
        Configuration = configuration; // <-- and we save that
    }
    
    // rest of the class ...
}

The whole point of IConfiguration is to provide a way through which the .NET Core app can read configuration from different sources: configuration files, environment variables, command line arguments, etc.

Using IConfiguration outside Startup

You can also inject an IConfiguration instance almost anywhere:

public class EmailSender 
{
    private string ClientKey { get; }
    private string ClientSecret { get; }
    
    public EmailSender(IConfiguration configuration)
    {
        ClientKey = configuration["ClientKey"];
        ClientSecret = configuration["ClientSecret"];
    }
    
    // rest of the class follows...
}

Let's just stop for a while and imagine an example: we're in a controller's action and need to send an email. We get an EmailSender instance from DI, and use it to send the email. The EmailSender gets email credentials from the IConfiguration abstraction. It doesn't need to know where it's coming from: appsettings.json, command-line arguments, environment variables, azure vault storage– could be coming from anywhere and EmailSender doesn't need to care about it.

There are still areas left wanting though:

  1. The configuration is not strongly typed: non-string values will need to be manually parsed.
  2. The email sender above requires ClientKey and ClientSecret to be defined using the exact configuration keys. So EmailSender is sort-of coupled with the way the actual configuration is set.

The solution to these is the Options Pattern.

Options Pattern

Think of this as an additional layer of abstraction. In the EmailService example, we'd replace IConfiguration with IOptions. IConfiguration knew how to get the right configuration from the right places for you, IOptions knows how to get the relevant configuration from IConfiguration for you.

Here's a simplified way to use it:

  1. Group related settings together in your appsettings.json:
    {
      "Email": { // <-- settings grouped under "Email"
        "ClientKey": "xxx",
        "ClientSecret": "xxx"
      }
    }
    
  2. Create a class to hold those settings:
    public class EmailOptions
    {
        public string ClientKey { get; set; }
        public string ClientSecret { get; set; }
    }
    
  3. Register the Options with the DI container:
    services.Configure<EmailOptions(Configuration.GetSection("EmailOptions"));
    
  4. Inject it into the service that needs it:
    public class EmailSender 
    {
        private string ClientKey { get; }
        private string ClientSecret { get; }
    
        public EmailSender(IOptions<EmailOptions> options) // <-- here
        {
            ClientKey = options.Value.ClientKey;
            ClientSecret = options.Value.ClientSecret;
        }
    
        // rest of the class follows...
    }
    

Follow this, and you won't see the need to inject IConfiguration anywhere outside Startup.