Develop Clean Command Line Applications with System.CommandLine. Clean CLI.
Tags: dotnet, cli, architecture, cleanarchitecture
Published: 2021-06-06 16:50:05

---
layout: post
title: Develop Clean Command Line Applications with System.CommandLine. Clean CLI.
published: true
shortinfo: In this blog post I will show you how to develop powerful console applications that could benefit of your existing Clean Architecture projects in a great way.
fullview: false
comments: true
hide-related: false
---

## TL;DR

You can use `System.CommandLine` to build console applications. The blog post explains how to build one on top of *Clean Architecture* solution. You can check out the sample, it contains more information and source code: <https://github.com/NikiforovAll/clean-cli-todo-example>.

---

As a developer, I quite often want to create a console application to try things out. Usually, it works well, but I always find myself in an inconvenient position. The `Program.cs` bloats in messy monster with actual useful nuggets of codes in between of code that works with `args` and some plumbing code. For some tasks, it works, but for others, I would like to suggest something more manageable and clean.

## Introduction to System.CommandLine

`System.CommandLine` gives you a great experience by providing essential functionality, such as parsing, invocation, and rendering. Let's see how we can write a simple "Todo List" application with it.

Here is the simple console application that prints "help" provided automatically by the `System.CommandLine`.

```csharp
// Program.cs
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;

var root = new RootCommand("Root command description");
root.Handler = CommandHandler.Create(() => root.Invoke("-h"));

var builder = new CommandLineBuilder(root);

var parser = builder.UseDefaults().Build();

await parser.InvokeAsync(args);
```

And the output:

```bash
$ dotnet run
app-name
  Root command description

Usage:
  app-name [options]

Options:
  --version       Show version information
  -?, -h, --help  Show help and usage information
```

The `Command` composition defines the structure and the way your app looks and feels. You can assign `ICommandHandler` to the command. It is used during the invocation phase and provides you model-binding functionality. I suggest you to go through <https://github.com/dotnet/command-line-api/blob/main/docs/How-To.md> and <https://github.com/dotnet/command-line-api/blob/main/docs/Features-overview.md> to get yourself more comfortable with this awesome library.

Let's move forward and see a slightly different version built on top of .NET Core [Generic Host](https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host).

The code below performs the following:

1. Initializes and configures `CommandLineBuilder`.
2. Plugs `IHostBuilder` into `CommandLineBuilder` so it can be used later to build an invocation pipeline.
3. Builds `Parser` based on the configured instance of `CommandLineBuilder`.
4. Invokes `Parser` with arguments provided by `Program.Main` method.

```csharp
var parser = BuildCommandLine()
    .UseHost(_ => Host.CreateDefaultBuilder(args), (builder) =>
    {
        builder.ConfigureServices((hostContext, services) =>
        {
            var configuration = hostContext.Configuration;
            // register other dependencies here
        })
        .UseCommandHandler<ExampleCommand, ExampleCommand.Handler>()
        
    }).UseDefaults().Build();
return await parser.InvokeAsync(args);

static CommandLineBuilder BuildCommandLine()
{
   var root = new RootCommand();
   root.AddCommand(new ExampleCommand()); 
   return new CommandLineBuilder(root);
}
```

The benefit of this style is that you can easily understand how CLI application is composed and what are the dependencies.

As you may notice, we hooked up the command and corresponding handler via `UseCommandHandler`. It allows us to resolve dependencies for command handlers.

```csharp
public class ExampleCommand : Command
{
   public ExampleCommand() : base(name: "example", "Example description") {}

   public new class Handler : ICommandHandler
   {
      private readonly IMediator meditor;

      public Handler(IMediator meditor) =>
            this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor));

      public async Task<int> InvokeAsync(InvocationContext context)
      {
         await this.meditor.Send(new ExampleQuery{});
         return 0;
      }
   }
}
```

With this knowledge in mind, let's move further with something more interesting functionality to implement. I would like to show you how to use "Clean Architecture" approach together with CLI applications, therefore Clean CLI. I will use <https://github.com/jasontaylordev/CleanArchitecture> as an example of Clean Architecture project, please investigate sample code based before continue reading the blog post <https://github.com/NikiforovAll/clean-cli-todo-example>.

## System.CommandLine ➕ Clean Architecture = Clean CLI

Our goal is to build "Todo List" application. 📃📝

Design goals:

* To use a CLI as UI for a Clean Architecture based solution.
* To design an application that clearly communicates (through code) implemented functionality (commands) and structural composition in general.
* To provide a first-class CLI interface user experience. Luckily, `System.CommandLine` helps with things such as "help text" generation and autocompletion.

Before going deeper into source code let's examine how to consume the todo-cli.

```bash
$ dotnet run -- -h
CleanCli.Todo.Console

Usage:
  CleanCli.Todo.Console [options] [command]

Options:
  --silent        Disables diagnostics output
  --version       Show version information
  -?, -h, --help  Show help and usage information

Commands:
  todolist  Todo lists management
  todoitem  Todo items management
  migrate   Migrates database
```

```bash
$ dotnet run -- todolist -h
todolist
  Todo lists management

Usage:
  CleanCli.Todo.Console [options] todolist [command]

Options:
  --silent        Disables diagnostics output
  -?, -h, --help  Show help and usage information

Commands:
  create       Creates todo list
  delete <id>  Deletes todo list
  get <id>     Gets a todo list
  list         Lists all todo lists in the system.
```

```bash
$ dotnet run -- todolist create -h
create
  Creates todo list

Usage:
  CleanCli.Todo.Console [options] todolist create

Options:
  -t, --title <title>  Title of the todo list
  --dry-run            Displays a summary of what would happen if the given command line were run.
  --silent             Disables diagnostics output
  -?, -h, --help       Show help and usage information
```

From the code perspective it looks like this ([full version](https://github.com/NikiforovAll/clean-cli-todo-example/blob/main/src/CleanCli.Todo.Console/Program.cs)):

```csharp
var runner = BuildCommandLine()
   .UseHost(_ => CreateHostBuilder(args), (builder) => builder
      .UseEnvironment("CLI")
      .UseSerilog()
      .ConfigureServices((hostContext, services) =>
      {
         services.AddCustomSerilog();
         var configuration = hostContext.Configuration;
         services.AddCli(); // Dependencies defined by CLI project (this).
         services.AddApplication(); // Register "Application" project.
         services.AddInfrastructure(configuration); // Register "Infrastructure" project.
      })
      .UseCommandHandler<CreateTodoListCommand, CreateTodoListCommand.Handler>()
      .UseCommandHandler<DeleteTodoListCommand, DeleteTodoListCommand.Handler>()
      .UseCommandHandler<ListTodosCommand, ListTodosCommand.Handler>()
      .UseCommandHandler<GetTodoListCommand, GetTodoListCommand.Handler>()
      .UseCommandHandler<SeedTodoItemsCommand, SeedTodoItemsCommand.Handler>()
      .UseCommandHandler<MigrateCommand, MigrateCommand.Handler>())
         .UseDefaults().Build();

static CommandLineBuilder BuildCommandLine()
{
   var root = new RootCommand();
   root.AddCommand(BuildTodoListCommands());
   root.AddCommand(BuildTodoItemsCommands());
   root.AddCommand(new MigrateCommand());
   root.AddGlobalOption(new Option<bool>("--silent", "Disables diagnostics output"));
   root.Handler = CommandHandler.Create(() => root.Invoke("-h"));

   return new CommandLineBuilder(root);
   // omitted for brevity
}
```

As result, the project is plugged through DI to console application and all features provided by `Application` project could be consumed from `Console` project.

The application functionality is added as following:

```csharp
// Application/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
   public static IServiceCollection AddApplication(this IServiceCollection services)
   {
      services.AddAutoMapper(Assembly.GetExecutingAssembly());
      services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
      services.AddMediatR(Assembly.GetExecutingAssembly());
      services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
      services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));

      return services;
   }
}
```

```csharp
// Infrastructure/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
   public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
   {
      services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlite(configuration.GetConnectionString("DefaultConnection"),
               b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));

      services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
      services.AddScoped<IDomainEventService, DomainEventService>();
      services.AddTransient<IDateTime, DateTimeService>();

      return services;
   }
}
```

Finally, we can add code that does actually something useful.

```csharp
public class CreateTodoListCommand : Command
{
   public IConsole Console { get; set; }

   public CreateTodoListCommand() : base(name: "create", "Creates todo list")
   {
      this.AddOption(new Option<string>(new string[] { "--title", "-t" }, "Title of the todo list"));
   }

   public new class Handler : ICommandHandler
   {
      private readonly IMediator meditor;

      public string Title { get; set; } // Conventional binding

      public Handler(IMediator meditor) =>
            this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor));

      public async Task<int> InvokeAsync(InvocationContext context)
      {
            await this.meditor.Send(new CreateTodoListCommand { Title = this.Title });
            return 0;
      }
   }
}
```

Underlying implementation from `Application` project.

```csharp
public class CreateTodoListCommand : IRequest<int>
{
   public string Title { get; set; }
}

public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, int>
{
   private readonly IApplicationDbContext context;

   public CreateTodoListCommandHandler(IApplicationDbContext context) =>
      this.context = context;

   public async Task<int> Handle(
      CreateTodoListCommand request,
      CancellationToken cancellationToken)
   {
      var entity = new TodoList { Title = request.Title };
      this.context.TodoLists.Add(entity);
      await this.context.SaveChangesAsync(cancellationToken);

      return entity.Id;
   }
}
```

If you are interested in this approach, I encourage you to investigate the source code on your own.

### Demo

Let's run `migrate` command to create local *todo.db* and seed initial data.

![migrate-command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/toklnuoj7tc5iizc4wdk.png)

Create todo list:

![create-todolist-command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inpmra3mj10ehxv9bx9g.png)

List commands:

![list-command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ln5svz80f75a83cdupwd.png)

See todo list details:

![list-command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/moipe8k7pt2btnk4zmp2.png)

I hope you find this blog post useful. Take care!

---

## Reference

* <https://github.com/dotnet/command-line-api>
* <https://clig.dev/>
* <https://spectreconsole.net/>
* <https://github.com/jasontaylordev/CleanArchitecture>
* <https://github.com/NikiforovAll/clean-cli-todo-example/tree/v0.1.0> - template-like version.