Announcing ConsoleApplicationBuilder, DI in console applications, simply

Published on Friday, January 17, 2025

Configurable Console Application

A recent interaction reminded me that using things like IServiceCollection to get dependency injection or ConfigurationManager to use appsettings.json and IOptions<T> in a .NET console application is a lot of work. You can use things like console worker project, but if you're implementing a simple console application, a background process seems over the top and unnatural.

I've added DI and configuration to console applications in the past, but it has always been troublesome and problematic. I had considered just documenting that process but thought the amount of work to simply create a NuGet package to do it for me seemed like it was about the same amount of work. A couple of weeks ago I started creating ConsoleApplicationBuilder—a type that implements similar functionality to WebApplicationBuilder and HostApplicationBuilder (that WebApplicationBuilder leverages). This post announces the result of that effort.

TL;DR: ConsoleApplicationBuilder and the associated dotnet new project template enable the use of DI in console applications simply and succinctly:

class Program(ILogger<Program> logger)
{
    static void Main(string[] args)
    {
        var builder = ConsoleApplication.CreateBuilder(args);
        var program = builder.Build<Program>();
        program.Run();
    }

    private void Run()
    {
        logger.LogInformation("Hello, World!");
    }
}

ConsoleApplicationBuilder is open-source and available on GitHub; you can find similar details there.

What prompted my latest foray into DI in a non-background console application was to inject a configured HttpClient into the application. ConsoleApplicationBuilder supports this easily and naturally:

class Program(ILogger<Program> logger, HttpClient httpClient)
{
    static async Task Main(string[] args)
    {
        var builder = ConsoleApplication.CreateBuilder(args);
        builder.Services.AddHttpClient<Program>(httpClient =>
        {
            httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
        });
        var program = builder.Build<Program>();
        await program.Run();
    }

    private async Task Run()
    {
        logger.LogInformation("Hello, World!");
        logger.LogInformation(await httpClient.GetStringAsync("todos/3"));
    }
}

## `dotnet new` Project Template

The `dotnet new consoleapp` project template supports appsettings out of the box and is installed like other `dotnet new` project templates:

```powershell
dotnet new install PRI.ConsoleApplicationBuilder.Templates

And to create a new console application that supports DI, Configuration, appsettings, etc., is as simple as:

dotnet new consoleapp -o Peter.ConsoleApp

ConsoleApplicationBuilder Without dotnet new consoleapp Project Template

When I implemented the dotnet new consoleapp project template, I wanted to make the created Program class as simple as possible, so I chose to re-use the Program class and build an instance of that class to inject services like ILogger<Program>. However, you don't need to use the template to use ConsoleApplicationBuilder, and you don't need to re-use Program as the type that it will build. You can add a reference to the PRI.ConsoleApplicationBuilder NuGet package and build a different type. For example:

class Program
{
    static async Task Main(string[] args)
    {
        var builder = ConsoleApplication.CreateBuilder(args);
        builder.Services.AddHttpClient<ToDoAdapter>(httpClient =>
        {
	        httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
        });
        var adapter = builder.Build<ToDoAdapter>();
		var todoText = await adapter.GetToDo(1);
	}
}

internal sealed class ToDoAdapter(ILogger<ToDoAdapter> logger, HttpClient httpClient)
{
	public async Task<string> GetToDo(int id)
	{
		if(logger.IsEnabled(LogLevel.Information))
		{
			logger.LogInformation("Getting todo with id {Id}", id);
		}
		var response = await httpClient.GetAsync($"todos/{id}");
		response.EnsureSuccessStatusCode();
		string responseText = await response.Content.ReadAsStringAsync();
		if (logger.IsEnabled(LogLevel.Information))
		{
			logger.LogInformation("Got response {ResponseText}", responseText);
		}
		return responseText;
	}
}

Here, instead of building a Program instance, we're building a ToDoAdapter instance that is injected with a configured logger and an specific HTTP client. Be sure to also add a reference to Microsoft.Extensions.Http if you want to use that code.

So, as you can see, ConsoleApplicationBuilder is very flexible.

Feedback

I'd love to hear your feedback, good or bad. I've modeled ConsoleApplicationBuilder as closely as possible to WebApplicationBuilder and HostApplicationBuilder so it follows their conventions, and usage should feel natural. But I'm open to suggestions on how to improve it. You can log questions and issues on GitHub.

comments powered by Disqus