FAESEL.COM
My journey of creating a .NET CLI tool

Date Published December 18th, 2020  Reading Time 10 Minutes

  1. cli
  2. azure
  3. queues
  4. table-storage
  5. containers
  6. blob
  7. azure-storage
  8. dotnet-tools
  9. az-lazy
  10. console
  11. commandline
  12. dotnet-tools
Az Lazy

Why I started building a CLI

As a .NET engineer, I work with Azure storage a lot, its versatility, ease of use, as well as cost makes it a common staple amongst developers. Its application is also widespread from leveraging queues on a basic console app to storing uploaded images from a web application.

Typically as an engineer, I have always interfaced with azure storage using Azure Storage Manager, but as a UI tool, its always been two clicks away from the information I need or was just slow to navigate.

So I ended up taking my destiny into my own hands and built a CLI tool called Az-Lazy.

Packages used when I started

So as with all projects, I started with a shopping list of packages I wanted to use/needed to make a great CLI experience.

1. CommandLineParser

Whilst there are many command parsers on the market, I found this implementation particular nice to use. The end result allows you to to get a command pattern similar to most of the Microsoft dotnet tools, azlazy connection --list.

Defining available commands is as easy as decorating a class with attributes, and the help options for each command is automatically generated for you (azlazy addcontainer --help).

[Verb("addcontainer", HelpText = "Creates a new storage container")] public class AddContainerOptions : ICommandOptions { [Option('n', "name", Required = true, HelpText = "Name of the container to create")] public string Name { get; set; } [Option('p', "publicAccess", Required = false, HelpText = "Options are None, Blob, BlobContainer")] public string PublicAccess { get; set; } }

2. Pastel

Splash of colour is always a sign of a great CLI experience and seeing red or green are key indicators of successful execution of a command. Pastel lets you do exactly that! its got a range of preset vibrant colours to choose from and allows you to easily RGB your console output.

The extension method style syntax is also great and doesn't distract you away from the code.

"You successfully deleted all your data".Pastel(Color.LightGreen);

3. ConsoleTables

As Az-Lazy deals with table storeage, it was only a matter of time till i needed to output a table to the console. After a quick search on Nuget Gallery ConsoleTables showed up. With a simple minimilist table implementation it was quick to get setup,

var table = new ConsoleTable("Id", "Blob", "Size"); table.AddRow(1, "dinopic1.jpg", 300) .AddRow(2, "dinopic2.jpb", 450); table.Write();

However, I later had to drop this package for something slightly more advanced (Alba.CsConsoleFormat) as I needed to word wrap large cells (particularly when displaying JSON snippets).

4. Alba.CsConsoleFormat

As mentioned above Alba.CsConsoleFormat was a replacement package to render tables. With great options to not only style your table with colours but also specify word wrap options, it was a good choice for rendering large volumes of data.

I did find its syntax a little verbose to work with,

var headerThickness = new LineThickness(LineWidth.Double, LineWidth.Single); var doc = new Document( new Span("Dinosaurs #") { Color = Yellow }, Order.Id, "\n", new Span("Type: ") { Color = Yellow }, Order.Customer.Name, new Grid { Color = Gray, Columns = { GridLength.Auto, GridLength.Star(1), GridLength.Auto }, Children = { new Cell("Id") { Stroke = headerThickness }, new Cell("Name") { Stroke = headerThickness }, new Cell("Count") { Stroke = headerThickness }, Order.OrderItems.Select(item => new[] { new Cell(item.Id), new Cell(item.Name), new Cell(item.Count) { Align = Align.Right }, }) } } ); ConsoleRenderer.RenderDocument(doc);

Buy my oh my does it produce a great console table 👨‍🎨

CsConsoleFormat table output

5. LiteDb

One of the requirements of Az-Lazy was to store a list of connections to be reused at any time. Since CLI's don't have any state, I needed a lightweight storage mechanism that can easily ship with the tool.

In comes LiteDb! With its entity framework style CRUD syntax, I really felt at home with this framework,

// Create your POCO class public class Dinosaur { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } using(var db = new LiteDatabase(@"DinoDb.db")) { var collection = db.GetCollection<Dinosaur>("dinosaurs"); var dinosaur = new Dinosaur { Id = 1 Name = "T-Rex", Age = 39 }; collection.Insert(dinosaur); var oldDinosaurs = col.Find(x => x.Age > 50); }

Since LiteDb stores all its data into one file, from a tool perspective it ensures your not littering your client's computer with files.

Packages used now

1. CommandLineParser

I've still continued to use CommandLineParser however it's starting to hit some limitations, namely nesting of commands. So something like azlazy connection add --name "test" --connection "connectionString" is not allowed.

Whilst this package has been great to get me started, I might start looking at other options as Az-Lazy starts supporting more complex commands.

2. LiteDb

It just works, still happy with this package. Really recommend it for CLI tools!

2. Spectre.Console

Spectre.Console was a big find for me, from the writer of Cake (Patrik Svensson) it's a one-stop-shop for all your CLI needs. Just to name a few of the features,

  • Console Colours
  • Progress bars
  • Tables
  • Prompts
  • Spinners

After discovering this package I ended up doing a NuGet cull 🪓 which is why this list now stops at 3 (That culls still in progress as I'm removing Pascal for Specters implementation).

I won't go into all the code samples for this, but here are a few, beginning with progress indicators (also note the syntax to colour the output).

await AnsiConsole.Progress() .StartAsync(async ctx => { // Define tasks var dinoTask = ctx.AddTask("[green]Uploading dinosaurs[/]"); while (!ctx.IsFinished) { // Simulate some work await Task.Delay(250); dinoTask.Increment(1.5); } });

Check out how it looks in action, it's great for long-running tasks.

Upload progress bar

Table's are also strightforward to create,

// Create a table var table = new Table(); // Add some columns table.AddColumn("Foo"); table.AddColumn(new TableColumn("Bar").Centered()); // Add some rows table.AddRow("Baz", "[green]Qux[/]"); table.AddRow(new Markup("[blue]Corgi[/]"), new Panel("Waldo")); // Render the table to the console AnsiConsole.Render(table);

Other great code snippets

The only thing lacking for me in Specter.Console was display a tree structure which is useful for folder hierarchies, (there is an open issue here if you want to show your interest) 🙏.

To fulfil this requirement, I found a great article by Andrew Lock which renders this structure, Creating an ASCII-art tree in C#. Here's what it looks like for me,

Printing Tree structure

Why I chose .NET

So initially I was torn between Node JS and C#, there was an especially compelling article by Twilio outlining how you can create a great CLI experience that was winning me over. They also went so far as suggesting some interesting packages to make use of, here's a snippet of whats comparable with what I used for az-lazy.

Whilst the NuGet ecosystem is not as diverse as npm, after discovering Spectre.Console, I found that its a one-stop-shop for most of those npm packages mentioned above.

The deciding factor for me however was down to the userbase I was targeting. I figured the majority of my userbase would be more familiar with dotnet than npm, and this solidified the decision. However, whatever platform you choose I believe theres enough packages available to make a great CLI experience.

How to create a .NET CLI tool

This guide shows you how to create a bare-bones .NET CLI tool.

1. Creating a new project

Let's start by creating a new directory and console application.

  1. mkdir barebonescli
  2. cd barebonescli
  3. dotnet new console
  4. The console app will already come with a standard-issue Console.Writeline("Hello World"); 😁

2. Package as a tool

What distinguishes your console app from a dotnet tool is purly the .csproj file. In particular the package as a tool option <PackAsTool>true</PackAsTool>, Id which needs to be unique across all the NuGet packages in the gallery <Id>azlazy</Id> and tool command name <ToolCommandName>azlazy</ToolCommandName>. The full .csproj should look like this,

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <RootNamespace>barebonescli</RootNamespace> <PackAsTool>true</PackAsTool> <ToolCommandName>barebones</ToolCommandName> <PackageOutputPath>./nupkg</PackageOutputPath> <Version>1.0.0</Version> <Id>barebonescli</Id> <Authors>Faesel Saeed</Authors> <Owners>Faesel Saeed</Owners> <Title>Demo app to show a bare bones CLI</Title> <Description>This great CLI can greet the world</Description> <Copyright>Copyright 2020 Faesel Saeed</Copyright> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageLicenseFile>LICENSE.txt</PackageLicenseFile> <PackageIconUrl>https://raw.githubusercontent.com/faesel/barebonescli/main/barebones/icon.png</PackageIconUrl> <PackageTags>barebones greeting cli</PackageTags> <RepositoryUrl>https://github.com/faesel/barebonescli.git</RepositoryUrl> <RepositoryType>git</RepositoryType> <RepositoryBranch>main</RepositoryBranch> <PackageProjectUrl>https://github.com/faesel/barebonescli</PackageProjectUrl> <PackageIcon>icon.png</PackageIcon> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" /> ... <None Include="LICENSE.txt" Pack="true" PackagePath="$(PackageLicenseFile)" /> <None Include="icon.png" Pack="true" PackagePath="\" /> </ItemGroup> </Project>

The Github links are fictional, you can replace them with your project.

To make your package more credible in the NuGet gallery there are other fields you can fill in. For the licence and package icon, you will need to place the files in the same project, the folder structure will look something like this,

barebonescli

  • barebonescli.csproj
  • Program.cs
  • icon.png
  • LICENCE.txt

3. Package your CLI

The next step is to create a NuGet package, if you specified a PackageOutputPath in your .csproj you will see the NuGet package in that folder.

  1. Run dotnet pack on the project

Once executed you should have a barebonescli.1.0.0.nupkg file.

  1. Click on your profile
  2. Select upload package and upload the nupkg file

Nuget package upload

You could also alternatively use the CLI to push with nuget push barebonescli.1.0.0.nupkg

Once uploaded it will be ready to install as soon as its indexed, dotnet tool install --global barebonescli --version 1.0.0 😎

From here the packages mentioned in this article can help you create a professional-looking CLI.

More about Az-Lazy

If you're interested in checking out Az-Lazy, you can install in with the command,

dotnet tool install --global az-lazy

Usefull links

Do checkout Nuget Must Haves command-line tagged package list, there's some great options in there that are not mentioned in this article.

Nuet Must Haves - CommandLine Packages

SHARE

RELATED ARTICLES