Developing Console Apps with .NET

It can be useful to create console apps to support backend processes such as data anonymisation or batch processing. This post demonstrates how to develop console apps in .NET, including support for built-in help, arguments, configuration, logging, dependency injection, and more.

Create a new app

Create a new console app named “AwesomeConsole” using the built-in .NET template. Create the app with the following commands:

dotnet new console --output AwesomeConsole
cd AwesomeConsole

Launch the new app with the following command:

dotnet run

The program displays the message “Hello, World!” – a good start.

Next, update the console app to display the name, version, and copyright message. Open Program.cs, and replace all code with the following code:

Console.WriteLine("Awesome CLI ⚡ 1.0.0");
Console.WriteLine("Copyright (C) 2022 JasonTaylorDev");
Console.WriteLine();

For demonstration purposes, this app will display weather forecasts. Create a new file named WeatherForecast.cs with the following code:

namespace AwesomeConsole;

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; } = string.Empty;
}

Next, create a new file named WeatherForecastService.cs and add the following code:

namespace AwesomeConsole;

public class WeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public IEnumerable<WeatherForecast> GetForecasts(int count)
    {
        var rng = new Random();

        var forecasts = Enumerable.Range(1, count).Select(index =>
            new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            });

        return forecasts;
    }
}

The above service generates random weather forecast data. Update the app to automatically display 5 weather forecasts. Open Program.cs and add the following code:

var service = new WeatherForecastService();

var forecasts = service.GetForecasts(5);

Console.WriteLine("Date\t\tTemp\tSummary");
Console.WriteLine("----------------------------------");
foreach (var forecast in forecasts)
{
    Console.WriteLine($"{forecast.Date.ToShortDateString()}\t{forecast.TemperatureC}\t{forecast.Summary}");
}

Console.WriteLine();

If you run the app now, the system will generate 5 forecasts. It would be better if the user could specify the number for required forecasts. This can be accomplished using command-line arguments. Update the call to GetForecasts as follows:

var forecasts = service.GetForecasts(int.Parse(args[0]));

Launch the app with the following command:

dotnet run 7

The app info and 7 generated weather forecasts will be displayed:

Awesome CLI ⚡ 1.0.0
Copyright (C) 2022 JasonTaylorDev

Date            Temp    Summary
----------------------------------
7/09/2022       11      Sweltering
8/09/2022       42      Balmy
9/09/2022       4       Balmy
10/09/2022      44      Mild
11/09/2022      -7      Scorching
12/09/2022      19      Warm
13/09/2022      38      Freezing

Ensure your console app is working before continuing to the next section.

Using the Command Line Parser library

In the previous section, the user can specify the required number of forecasts using command-line arguments. It works, but it’s not user-friendly – exceptions will occur for missing or invalid arguments. You could update the code to ensure that the args are parsed correctly, however, you won’t need to, because there is a great library for that – Command Line Parser Library.

The Command Line Parser Library supports a standardised approach for parsing command-line options in .NET. Install the package using the following command:

dotnet add package CommandLineParser

Create a new file named Options.cs, and define the arguments to be parsed as follows:

using CommandLine;
using CommandLine.Text;

namespace AwesomeConsole;

public class Options
{
    // Models a command line value.
    [Value(0, MetaName = "count", Required = true, HelpText = "The number of weather forecasts to display.")]
    public int Count { get; set; }

    // Usage provide meta data for help screen.
    [Usage(ApplicationAlias = "forecasts")]
    public static IEnumerable<Example> Examples => new List<Example>
    {
        new Example("Display local weather forecasts",
            new Options { Count = 5 })
    };
}

Next, update Program.cs as follows:

using AwesomeConsole;
+ using CommandLine;

+ Parser.Default.ParseArguments<Options>(args).WithParsed(options =>
+ {
    Console.WriteLine("Awesome CLI ⚡ 1.0.0");
    Console.WriteLine("Copyright (C) 2022 JasonTaylorDev");
    Console.WriteLine();

    var service = new WeatherForecastService();

    var forecasts = service.GetForecasts(int.Parse(args[0]));

    Console.WriteLine("Date\t\tTemp\tSummary");
    Console.WriteLine("----------------------------------");
    foreach (var forecast in forecasts)
    {
        Console.WriteLine($"{forecast.Date.ToShortDateString()}\t{forecast.TemperatureC}\t{forecast.Summary}");
    }

    Console.WriteLine();
+ });

The app now supports standard command-line parsing and includes options to display help and the app version. Let’s take it for a test drive. Run the app normally, requesting 7 or so forecasts:

dotnet run 7

As before, 7 forecasts are displayed. Next, display the built-in help, by running the app without any arguments using the following command:

dotnet run

The help text for the app is displayed:

Awesome CLI 1.0.0
Copyright (C) 2022 JasonTaylorDev
USAGE:
Display local weather forecasts:
  supercharged 5

  --help            Display this help screen.

  --version         Display version information.

  count (pos. 0)    Required. The number of weather forecasts to display.

Finally, test options parsing by providing an invalid count:

dotnet run n

The program will display the following:

Awesome CLI 1.0.0
Copyright (C) 2022 JasonTaylorDev

ERROR(S):
  A value not bound to option name is defined with a bad format.
  A required value not bound to option name is missing.
USAGE:
Display local weather forecasts:
  supercharged 5

  --help            Display this help screen.

  --version         Display version information.

  count (pos. 0)    Required. The number of weather forecasts to display.

You can see it was quite easy to add options parsing, help text, and version info with the help of CommandLineParser. There is a lot more to this library, so be sure to check out the documentation.

Working with configuration

The console app template does not include support for configuration. Regardless, it is simple to get configuration up and running by using the .NET Generic Host. By creating a host, you will effectively create an object that encapsulates app resources and lifetime functionality such as:

  • Configuration
  • Logging
  • Dependency Injection (DI)

First, install the required package:

dotnet add package Microsoft.Extensions.Hosting

Then, update Program.cs as follows:

using AwesomeConsole;
using CommandLine;
+ using Microsoft.Extensions.Hosting;

Parser.Default.ParseArguments<Options>(args).WithParsed(options =>
{
    Console.WriteLine("Awesome CLI ⚡ 1.0.0");
    Console.WriteLine("Copyright (C) 2022 JasonTaylorDev");
    Console.WriteLine();

+     var builder = Host.CreateDefaultBuilder(args);

+     var app = builder.Build();
    ...

With the above change, support for configuration is in place.

Update the app to allow the temperature to be displayed in celsius or fahrenheit, by specifying the appropriate unit through configuration. Create a new configuration file named appsettings.json and add the following configuration:

{
  "Unit": "Celsius"
}

You could also specify “Fahrenheit” if preferred. By default, configuration files such as appsettings.json will not be copied to the output directory. To include appsettings.json, update AwesomeConsole.csproj as follows:

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

Next, create a new file named TemperatureUnit.cs and add the following enum:

namespace AwesomeConsole;

public enum TemperatureUnit
{
    Celsius,
    Fahrenheit
}

Finally, update Program.cs to retrieve the configured unit value:

using AwesomeConsole;
using CommandLine;
+ using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Parser.Default.ParseArguments<Options>(args).WithParsed(options =>
{
    Console.WriteLine("Awesome CLI ⚡ 1.0.0");
    Console.WriteLine("Copyright (C) 2022 JasonTaylorDev");
    Console.WriteLine();

    var builder = Host.CreateDefaultBuilder(args);

    var app = builder.Build();

+    var configuration = app.Services.GetRequiredService<IConfiguration>();

+    var unit = configuration.GetValue<TemperatureUnit>("Unit");

    var service = new WeatherForecastService();

    var forecasts = service.GetForecasts(int.Parse(args[0]));

    Console.WriteLine("Date\t\tTemp\tSummary");
    Console.WriteLine("----------------------------------");
    foreach (var forecast in forecasts)
    {
+        var temperature = unit == TemperatureUnit.Celsius 
+            ? forecast.TemperatureC
+            : forecast.TemperatureF;

+        Console.WriteLine($"{forecast.Date.ToShortDateString()}\t{temperature}\t{forecast.Summary}");
    }

    Console.WriteLine();
});

Launch the app and ensure it works correctly before continuing to the next section.

Working with dependency injection and logging

In the previous section, you created a .NET Generic Host and so support for logging and dependency injection is already in place. Let’s add logging for any request to get weather forecasts.

Update the WeatherForecastService as follows:

+ using Microsoft.Extensions.Logging;

namespace Forecasts;

public class WeatherForecastService
{
    private readonly string[] _summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

+     private readonly ILogger _logger;

+     public WeatherForecastService(ILogger<WeatherForecastService> logger)
+     {
+         _logger = logger;
+     }

    public IEnumerable<WeatherForecast> GetForecasts(int count)
    {
+         _logger.LogInformation("Getting {count} forecasts.", count);

        var rng = new Random();

        var forecasts = Enumerable.Range(1, count).Select(index =>
            new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = _summaries[rng.Next(_summaries.Length)]
            });

        return forecasts;
    }
}

Next, register WeatherForecastService with dependency injection by updating Program.cs as follows.

    var builder = Host.CreateDefaultBuilder(args);

+   // Add service to the container    
+   builder.ConfigureServices(services =>
+   {
+       services.AddTransient<WeatherForecastService>();
+   });
    
    var app = builder.Build();

    var configuration = app.Services.GetRequiredService<IConfiguration>();

    var unit = configuration.GetValue<TemperatureUnit>("Unit");

-   var service = new WeatherForecastService();
+   var service = app.Services.GetRequiredService<WeatherForecastService>();

Launch the app and ensure it works correctly. You should see output similar to the following:

Awesome CLI ⚡ 1.0.0
Copyright (C) 2022 JasonTaylorDev

info: AwesomeConsole.WeatherForecastService[0]
      Getting 3 forecasts.
Date            Temp    Summary
----------------------------------
8/09/2022       10      Freezing
9/09/2022       31      Hot
10/09/2022      7       Hot

Next steps

In this post, I have demonstrated how to develop powerful console apps with built-in help, options handling, configuration, logging, and dependency injection. If you would like to learn more, take a look at some of the following resources:

Thanks for reading, please feel free to post any questions or comments below.