Even if the main things I spend time building are web applications and APIs, I often also create some console applications: sometimes to do some benchmarks, others to test some features without messing up the main projects, or lastly, to implement some small tools (I prefer creating a small app and using a traditional programming language over creating a bunch of scripts).
What I never try to do however, is make said console applications pretty 😛. Most of the times, it’s not worth it, but even when it is, the further I went was using some helper libraries to parse command line arguments.
This is where Spectre.Console comes in. Came across this library not too long ago and was intrigued, so added it to my ever-growing “to explore” list, and having finally had some time to play a bit with it, here comes the obligatory blog post to share that this is a pretty cool library!
From the docs:
Spectre.Console is a .NET Standard 2.0 library that makes it easier to create beautiful console applications.
And it doesn’t disappoint!
Besides the ability to easily handle input arguments (much like the CommandLineParser library I’ve used before), it also provides support to render complex elements in the command line, like tables, trees or progress bars, as well as ways to prompt the user for information, be it in text form or selecting an option.
Enough with the chitchat, let’s look at a sample with a bunch of these features in play. To have a relatable context for the sample, I went with one of the last types of console applications I worked on: a migration tool.
The gist of it is, we have some data we want to migrate to a new format, for example from one database to another, so we want to create a tool to help us do this, targeting multiple environments.
At the end, we’ll get this:
To make the sample a bit more relatable than just showing random things happen in the console, came up with this data migration context.
Before diving into Spectre.Console, let’s quickly get up to speed with the context, so it all makes some sense.
We have a
SampleMigrator class, abstracting the actual migration work (which will actually just be a bunch of
Task.Delays to simulate stuff). It provides a way to connect to an environment, gather some initial information, do the actual migration and then disconnect.
The migration process reports after each item is migrated, so we can provide feedback on the status.
The overall API for this feature set looks like the following (implementation removed for brevity):
DisposeAsync handle the connection lifetime,
GatherMigrationInformationAsync gets some initial information, while
MigrateAsync handles the migration process, returning an
IAsyncEnumerable to report the status.
This is simpler than an actual data migration would likely be, but it should be complex enough to make the sample console application interesting 🙂.
Setup application and available commands
With all this intro and context out of the way, let’s build the console application!
First thing, install Spectre.Console. In the command line, on the root of the project, this would be:
Now in our application, we want to expose a couple of commands, migrate and rollback (even if we’re implementing only the former).
We set them up as follows:
AddCommand method requires us to specify a generic type that implements
ICommand, getting as a parameter the name of the command, i.e. what we’ll write in the command line to execute it.
We can implement
ICommand directly, or we can inherit from some abstract classes that already have some boilerplate code in place. These are
AsyncCommand, but also
AsyncCommand<TSettings>, in which
TSettings allow us to specify some settings, like the parameters and options the command accepts.
The migrate command is defined as follows:
If you recall from the video I embedded above, I execute the application with the line:
migrate selects the command, then I’m using options, namely the
-u/--username option to immediately provide the username to the application. Could do the same with the password and environment, but could’t show off all of the things happening in the demo.
None of the options is required, nor am I adding validation (which I could, by overriding
Validate in the
Settingsclass), cause we’re going to ask the user for any missing/incorrect value.
As for those description attributes, they’re used if we use the help option:
Prompting user for information
As just mentioned, none of the command options is required, so let’s ask the user for anything missing. We’ll make use of Spectre.Console’s prompts to make these requests.
At the top of
ExecuteAsync, started with the following, asking for any missing options:
And implemented these
AskXYZ using the following local functions:
As we’ll see through the rest of the post,
AnsiConsole is the entry point for interactions with the console.
For username and password, we’re calling
AnsiConsole.Prompt and passing in a
TextPromp, where we provide the text to show the user and expect a
string in return. We can define a validation function, so if the user enters incorrect information, it’ll be refused (as you can see in the video at the top of the post). For the password, we also call the
Secret extension method, so this prompt is treated as such, not showing the value on the screen.
For the environment, as we have an enum, instead of having the user type the value, we can use a
SelectionPrompt, so the user needs only to move through the options and select the desired one.
Presenting information and reporting progress
So, asking the user for some inputs in a couple of different ways? Check! Now let’s look at some ways to present information, as well as provide progress feedback, so the user knows things are happening, instead of just hoping the application is actually doing something 😛.
After we gather this initial information from the user, it’s probably a good idea to get the user to double check things, to ensure things are well configured before starting a migration. We could, for example, present the summary in a table format.
With a small number of lines, we can get that:
Then, we can ask the user for confirmation (resorting again to a selection prompt) and get on with it.
With all info in hand, it’s time to begin performing the migration steps, using the previously introduced
As you might have noticed, all of
SampleMigrator’s methods are async (in multiple variations), which immediately gives us an hint that they’re good candidates to make use of Spectre.Console’s progress reporting features.
DisposeAsync, which return
ValueTask, we can use a simple spinner, just to signal things are happening. We can use the
Status component for that.
Nice and easy! But it gets better 🙂. Spinners are an improvement over no feedback at all, but even better is to actually have an idea of the amount of work done and the amount remaining. As we know the amount of… “things” to migrate (returned by
MigrateAsync returns an
IAsyncEnumerable with information about each migrated item, we can make use of the
Progress component as follows.
We start a progress task associated with the migration process. To have the progress accurately displayed without messing with math to find the percentage of work done, we can pass the maximum value to the
AddTask call on the context. Then, for each processed item, we call
Increment on the progress task.
When our migration finally finishes, we can present the results to the user. As we have two possibilities, success or failure, we can present it with a simple bar chart.
Don’t know about you, but I’ve never built such a pretty console application. As I mentioned in the beginning, the most I’ve done was using some helper libraries to parse command line arguments, and maybe hide passwords or adding a couple of colors here and there, but what I’ve played with for this post is just next level stuff! 😃
It goes without saying that there are more features I didn’t go into, but just this brief look has me sold on Spectre.Console, which I’ll certainly consider next time I need to build a console application that could use better usability.
Links in the post:
The source code for this post is in the SpectreConsoleSample repository.
Thanks for stopping by, cyaz!