Write a modern .Net Console application
Building a modern console application has never been easier.
In this article we will be creating a console application using Generic Host
and System.Commandline
as well as leveraging dependency injection, configuration and logging. If you wish to see the final result you can just check out the Github project.
1. Create a new dotnet console application
dotnet new console -n MyConsoleApp
cd MyConsoleApp
Add System.Commandline
packages
dotnet add package Microsoft.Extensions.Hosting
dotnet add package System.CommandLine --prerelease
dotnet add package System.CommandLine.Hosting --prerelease
At the time of the writing System.CommandLine is in Preview
. The API may change substantially before it's released.
2. Use Generic Host with a Command
Let's create a new file and call it HashNodeGreetCommand
. This will hold the command class and our command handler.
using System.CommandLine;
using System.CommandLine.Invocation;
public class HashNodeGreetCommand : Command
{
public HashNodeGreetCommand() : base("greet", "Greet hashnode")
{
AddOption(new Option<string>(new string[] { "--message", "-m" }, "The greeting message"));
}
public new class Handler : ICommandHandler
{
public int Invoke(InvocationContext context)
{
Console.WriteLine("Hello from Invoke");
return 0;
}
public Task<int> InvokeAsync(InvocationContext context)
{
return Task.FromResult(Invoke(context));
}
}
}
Change Program.cs
to this
using Microsoft.Extensions.Hosting;
var parser = BuildCommandLine()
.UseHost(_ => Host.CreateDefaultBuilder(args), builder => builder
.ConfigureServices((hostContext, services) =>
{
})
.UseCommandHandler<HashNodeGreetCommand, HashNodeGreetCommand.Handler>())
.UseDefaults()
.Build();
return await parser.InvokeAsync(args);
static CommandLineBuilder BuildCommandLine()
{
var rootCommand = new RootCommand("HashNode console application");
rootCommand.AddCommand(new HashNodeGreetCommand());
return new CommandLineBuilder(rootCommand);
System.CommandLine.Hosting
adds a few methods that allow us to use the Generic Host with System.CommandLine
.
The first one is UseHost
, this will allow us to set up dependency injection, logging, configuration, etc.
The second method from this package that we are going to use is UseCommandHandler
. We are going to use for setting up the commands and command handlers. If we have multiple commands, we can chain call this method multiple times.
If we run the program, we should see "Hello from Invoke" printed in the console.
After the parser is built, we call InvokeAsync
and pass in the args
.
3. Use Dependency Injection
First, let's create a simple service with a single method Run
that will use the injected logger to print out a message.
public class HashNodeService
{
private readonly ILogger<HashNodeService> _logger;
public HashNodeService(ILogger<HashNodeService> logger)
{
_logger = logger;
}
public void Run()
{
_logger.LogInformation("Starting HashNode service");
}
}
In Program.cs
register the service in the ConfigureServices
method. services.AddSingleton<HashNodeService>();
Inject the service into the command handler and call Run
public new class Handler : ICommandHandler
{
private readonly HashNodeService _hashNodeService;
public Handler(HashNodeService hashNodeService)
{
_hashNodeService = hashNodeService;
}
public int Invoke(InvocationContext context)
{
_hashNodeService.Run();
return 0;
}
public Task<int> InvokeAsync(InvocationContext context)
{
return Task.FromResult(Invoke(context));
}
}
4. Configuration using appsettings.json and IOptions
Suppose we have a configuration for our service that is loaded from appsettings.json or some other configuration provider, but we also want to be able to overwrite it by using command line arguments.
Because Host.CreateDefaultBuilder
loads command line args the last, we can use this to our advantage.
Let's start by adding appsettings.json file to the project (make sure it is copied to the output directory) and add some configuration. For example:
{
"PersonToGreet": "Hash"
}
Create the config class
public class HashNodeConfig
{
public string? PersonToGreet { get; set; }
}
Register the configuration in the ConfigureServices
method
.ConfigureServices((hostContext, services) =>
{
var configuration = hostContext.Configuration;
services.Configure<HashNodeConfig>(configuration);
services.AddSingleton<HashNodeService>();
})
Inject the config into the service and log it
public class HashNodeService
{
private readonly HashNodeConfig _config;
private readonly ILogger<HashNodeService> _logger;
public HashNodeService(IOptions<HashNodeConfig> options, ILogger<HashNodeService> logger)
{
_config = options.Value;
_logger = logger;
}
public void Run()
{
_logger.LogInformation("Starting HashNode service");
_logger.LogInformation("Hello: {personToGreet}", _config.PersonToGreet);
}
}
If we run the program right now it will print the configuration from the appsettings.json dotnet run -- greet
will print Hello: Hash
Next, we will add the ability to overwrite the option from the command line. In the HashNodeGreetCommand
class, add a new option:
AddOption(new Option<string>("--personToGreet", "Person to greet"));
Now, if we run dotnet run -- greet --personToGreet Node
it will overwrite the PersonToGreet
configuration and it will print Hello: Node
5. Log using Serilog
We will be going to log using Serilog and configure it in appsettings.json
. First, install the Serilog dependencies.
dotnet add package Serilog.Extensions.Hosting
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Settings.Configuration
Update Program.cs
to use Serilog with the configuration from appsettings.json
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Parsing;
using ConsoleAppBoilerplate;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
var parser = BuildCommandLine()
.UseHost(_ => Host.CreateDefaultBuilder(args), builder => builder
.ConfigureServices((hostContext, services) =>
{
var configuration = hostContext.Configuration;
services.Configure<HashNodeConfig>(configuration);
services.AddSingleton<HashNodeService>();
})
.UseSerilog((hostingContext, _, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration))
.UseCommandHandler<HashNodeGreetCommand, HashNodeGreetCommand.Handler>())
.UseDefaults()
.Build();
return await parser.InvokeAsync(args);
static CommandLineBuilder BuildCommandLine()
{
var rootCommand = new RootCommand("HashNode console application");
rootCommand.AddCommand(new HashNodeGreetCommand());
return new CommandLineBuilder(rootCommand);
}
Conclusion
We have set up a .Net Console application that uses System.CommandLine, offering us a lot of features out of the gate, like parsing the input and displaying help text. On top of that, we leveraged the Generic Host that gave us more goodies like Dependency injection (DI), Logging, Configuration, App shutdown etc. We also showed how we can use configuration and overwrite it from the command line, as well as setting up Serilog, useful when building serious applications.
Subscribe to my newsletter
Read articles from Cornel Cocioaba directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by