How to use configuration with C# 9 top-level programs

In a C# 9 deep dive, we talk about how top-level programs work with status codes, async, arguments, and local functions.

Dave Brock
Dave Brock

I’ve been working with top-level programs in C# 9 quite a bit lately. When writing simple console apps in .NET 5, it allows you to remove the ceremony of a namespace and a Main(string[] args) method. It’s very beginner-friendly and allows developers to get going without worrying about learning about namespaces, arrays, arguments, and so on. While I’m not a beginner—although I feel like it some days—I enjoy using top-level programs to prototype things quickly.

With top-level programs, you can work with normal functions, use async and await, access command-line arguments, use local functions, and more. For example, here’s me working with some arbitrary strings and getting a random quote from the Ron Swanson Quotes API:

using System;
using System.Net.Http;

var name = "Dave Brock";
var weekdayHobby = "code";
var weekendHobby = "play guitar";
var quote = await new HttpClient().GetStringAsync("https://ron-swanson-quotes.herokuapp.com/v2/quotes");

Console.WriteLine($"Hey, I'm {name}!");
Console.WriteLine($"During the week, I like to {weekdayHobby} and on the weekends I like to {weekendHobby}.");
Console.WriteLine($"A quote to live by: {quote}");

Add configuration to a top-level program

Can we work with configuration with top-level programs? (Yes, should we is a different conversation, of course.)

To be clear, there are many, many ways to work with configuration in .NET. If you’re used to it in ASP.NET Core, for example, you’ve most likely done it from constructor dependency injection, wiring up a ServiceCollection in your middleware, or using the Options pattern—so you may think you won’t be able to do it with top-level programs.

Don’t overthink it. Using the ConfigurationBuilder, you can easily use configuration with top-level programs.

Let’s create an appsettings.json file to replace our hard-coded values with configuration values.

{
    "Name": "Dave Brock",
    "Hobbies": {
        "Weekday": "code",
        "Weekend": "play guitar"
    },
    "SwansonApiUri": "https://ron-swanson-quotes.herokuapp.com/v2/quotes"
}

Then, make sure your project file has the following packages installed, and that the appSettings.json file is being copied to the output directory:

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

In your top-level program, create a ConfigurationBuilder with the appropriate values:

var config = new ConfigurationBuilder()
                 .SetBasePath(Directory.GetCurrentDirectory())
                 .AddJsonFile("appsettings.json")
                 .Build();

With a config instance, you’re ready to simply read in your values:

var name = config["Name"];
var weekdayHobby = config.GetSection("Hobbies:Weekday");
var weekendHobby = config.GetSection("Hobbies:Weekend");
var quote = await new HttpClient().GetStringAsync(config["SwansonApiUri"]);

And here’s the entire top-level program in action:

using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Net.Http;

var config = new ConfigurationBuilder()
                 .SetBasePath(Directory.GetCurrentDirectory())
                 .AddJsonFile("appsettings.json")
                 .Build();

var name = config["Name"];
var weekdayHobby = config.GetSection("Hobbies:Weekdays");
var weekendHobby = config.GetSection("Hobbies:Weekends");
var quote = await new HttpClient().GetStringAsync(config["SwansonApiUri"]);

Console.WriteLine($"Hey, I'm {name}!");
Console.WriteLine($"During the week, I like to {weekdayHobby.Value}" +
        $" and on the weekends I like to {weekendHobby.Value}.");
Console.WriteLine($"A quote to live by: {quote}");

Review the generated code

When throwing this in the ILSpy decompilation tool, you can see there’s not a lot of magic here. The top-level program is merely wrapping the code in a Main(string[] args) method and replacing our implicit typing:

using System;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

[CompilerGenerated]
internal static class <Program>$
{
  private static async Task <Main>$(string[] args)
  {
    IConfigurationRoot config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
    string name = config["Name"];
    IConfigurationSection weekdayHobby = config.GetSection("Hobbies:Weekday");
    IConfigurationSection weekendHobby = config.GetSection("Hobbies:Weekend");
    string quote = await new HttpClient().GetStringAsync(config["SwansonApiUri"]);
    Console.WriteLine("Hey, I'm " + name + "!");
    Console.WriteLine("During the week, I like to " + weekdayHobby.Value + " and on the weekends I like to " + weekendHobby.Value + ".");
    Console.WriteLine("A quote to live by: " + quote);
  }
}

Wrap up

In this quick post, I showed you how to work with configuration in C# 9 top-level programs. We showed how to use a ConfigurationBuilder to read from an appsettings.json file, and we also reviewed the generated code.

CSharpCSharp 9

Consider subscribing to The .NET Stacks, my free weekly newsletter. I write about news and trends, interview community leaders, and catch you up fast. (No spam, ever, and unsubscribe whenever you want.)


    Consider subscribing to The .NET Stacks, my free weekly newsletter. I write about news and trends, interview community leaders, and catch you up fast. (No spam, ever, and unsubscribe whenever you want.)