.Net Core Console Applications with Dependency Injection

I recently ran into a need to author a console app that relied upon a .NET Core library, and that library exposed its functionality using an extension to IServiceCollection. This sent me down a rabbit hole on how to build a .NET Core console application supporting the following features:

  1. Support of dependency injection via Microsoft.Extensions.DependencyInjection
  2. Robust command line argument parsing using CommandLine
  3. Logging independently of Console I/O using Serilog

The project referred to in this article is on GitHub and demonstrates a console application that either calculates a mathematical expression or generates some statistical values for a set of numbers. For what this application actually does, dependency injection (and even .NET Core) are overkill, but it serves as a functional example.

Application Structure

Using the structure described below, I am:

  1. Configuring services for the application
  2. Parsing command line arguments and generate task factory to execute the desired command
  3. Executing the task and exit indicating success or failure

Most examples I saw online discussed using IHostLifetime to execute a long-running service, daemon, etc. In my use case, I wanted to simply execute a Task and exit. If you have a long-running application, you can adapt this example to leverage services implementing IHostLifetime.

Structure for a Dependency Injected Console Application

If you are familiar with using IHostBuilder and IServiceCollection .NET Core, most of the above will look familiar to you.

The Log.Logger call is for Serilog, the IConsoleOutput service is a simple abstraction layer over System.Console. In the tasks, I inject both IConsoleOutput and ILogger separately so that I can control what goes to the console and what goes out for logging.

Setting up separate logging for a console application may seem like overkill, but if you are writing a console application designed to be called from a shell/batch script, you may not want to “litter” your output with diagnostic information.

The Parser.Default.Arguments is where the command line arguments are parsed and we create the associated task (implementing ITaskFactory) for the desired operation, which is described below.

After launching the build of IHost, we then get the ITaskFactory service created, launch a task, and exit with the results.

Command Line Parsing

The most robust solution I have found for command line argument parsing is the excellent Command Line Parser library. This is a deep and broad mechanism for parsing arguments and displaying help. This example demonstrates the use of two different “verbs” (for evaluating an expression or generating statistics) each with different options and command line arguments. You can specify named and positional arguments. For more information on usage, consult the Command Line Parser library’s Wiki.

Arguments for the “calc” verb

You can create a class like this for each command you want your application to process, and then pass the class names in as generic parameters to `Parser.Default.ParseArguments`. Instances of these classes will be generated when command line arguments are parsed and can be passed or injected into your tasks factories.

Using CommnadLineParser to Configure Tasks

Setting up Task Factories

When using dependency injection, in general we need to define how things are going to before we use them. In this demonstration, we want to look at the command line arguments, figure out the task that we want to execute when configuring services, and subsequently execute that task after all of our services are configured.

To accomplish this, I created an interface to implement that will generate a Task, and that task will return an integer value to use as the application exit code. A key here is that we’re not performing the task here, we are returning that will launch the task after everything is configured.

A Simple Factory to Launch a Task
  1. Create a task factory implementing ITaskFactory in ConfigureServices
  2. Launch a task using the task factory after we’ve configured everything
Configuring a Task Factory and Launch a Task

The ITaskFactory approach lets us avoid executing the program when configuring services. It also makes it so what we don’t have to know what the task is outside of ConfigureServices. Since this is a command line utility that executes once and then exits, everything is singleton (stateless)

Tasks

Once everything is set up, doing stuff a task is pretty mundane. The constructor gets all our dependencies (including the parsed command line argument options). The Launch method creates a Task that does what needs to be done and returns 0 on happy path and -1 if something bad happened.

Example Task to Evaluate a Mathematical Expression

Conclusion

And that’s about it. As far as the application itself, if you are actually wanting to try this demonstration, you can go to the sample.console directory and:

  • dotnet run -- 100 + 200 / 2 (Returns 200)
  • dotnet run -- "(100 + 200) / 2" (Returns 150, you’ll need the quotes for parenthesis and asterisks)
  • dotnet run -- calc 500 + 2 (Returns 502, the “calc” is unnecessary but shown for completeness)
  • dotnet run -- -f json 100/2 (Returns {Value”:50})
  • dotnet run -- stats 100 200 300 400 500 (Returns Count=5, Mean=300, Variance=25000, StandardDeviation=158.11388300841898, Skewness=0, Kurtosis=-1.2000000000000002, Maximum=500, Minimum=100)

and so on..

I certainly wouldn’t use this as-is for anything “real” but hopefully this was useful as a demonstration.

Again, here’s the link to the project on GitHub.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store