Lesson 1.4: Async Commands

To this point, we have been using synchronous commands, but the Spectre.Console.Cli library also supports asynchronous commands. For anyone familiar with the async/await concepts in .NET, these commands follow the same pattern. When our commands use asynchronous APIs (for file I/O or web service calls), then we also need our commands to be asynchronous. With asynchronous commands the code doesn’t lock, so UI can be updated and parallel code can be run.

In this lesson, we will look at writing and configuring asynchronous commands and how they are processed in our command-line app. We will continue to use the Contoso University commands that we defined in the previous lesson.

Reminder: you can find the source for this lesson in our Azure DevOps repo: Cli.Lesson1.4 – Repos (azure.com).

First, let’s start by creating a new Console App in Visual Studio named Cli.Lesson1.4… review project creation in Lesson 1.1, for the full details on getting setup.

Build Async Commands

We are going to create four separate commands in the the project Commands folder. All of these commands will derive from AsyncCommand rather than Command (which we have used up to now), and override the ExecuteAsync method instead of Execute.

First, create the StudentAddCommand class:

using Spectre.Console.Cli;
using Spectre.Console;
using System;
using System.ComponentModel;
using System.Threading.Tasks;

namespace Cli.Lessons.Commands
{
    public class StudentAddCommand : AsyncCommand<StudentAddCommand.Settings>
    {
        public class Settings : CommandSettings
        {
            [CommandArgument(0, "<ID>")]
            [Description("New student unique id.")]
            public int Id { get; set; }

            [CommandArgument(1, "<FIRST-NAME>")]
            [Description("New student first name.")]
            public string FirstName { get; set; }

            [CommandArgument(2, "<LAST-NAME>")]
            [Description("New student last name.")]
            public string LastName { get; set; }

            [CommandOption("-e|--enrollment <DATE>")]
            [Description("New student enrollment date.")]
            public DateTimeOffset? EnrollmentDate { get; set; }
        }

        public override Task<int> ExecuteAsync(CommandContext context, Settings settings)
        {
            var enrolled = settings.EnrollmentDate ?? DateTimeOffset.Now;
            AnsiConsole.MarkupLine(
                $"[bold]Add Student =>[/] id[[{settings.Id}]], " +
                $"name[[{settings.FirstName} {settings.LastName}]] " +
                $"enrolled[[{enrolled}]]");
            return Task.FromResult(0);
        }
    }
}

We notice that the bulk of this class looks exactly like the synchronous version (in lesson 1.3), with the following changes:

  • In line #9, we derive our command from the AsyncCommand base class.
  • In line #30, we override that class’s ExecuteAync method and return a Task<int>.
  • In line #37, we return a Task with the appropriate value. We use Task.FromResult because this simple example code doesn’t actually call any async APIs.

We will see that all of the async commands below make these same changes…

Then, create the StudentDeleteCommand class:

using Spectre.Console.Cli;
using Spectre.Console;
using System.ComponentModel;
using System.Threading.Tasks;

namespace Cli.Lessons.Commands
{
    public class StudentDeleteCommand : AsyncCommand<StudentDeleteCommand.Settings>
    {
        public class Settings : CommandSettings
        {
            [CommandArgument(0, "<ID>")]
            [Description("Id used to remove student.")]
            public int Id { get; set; }
        }

        public override Task<int> ExecuteAsync(CommandContext context, Settings settings)
        {
            AnsiConsole.MarkupLine($"[bold]Delete Student =>[/] id[[{settings.Id}]]");
            return Task.FromResult(0);
        }
    }
}

Next, create the StudentListCommand class:

using Spectre.Console.Cli;
using Spectre.Console;
using System.Threading.Tasks;

namespace Cli.Lessons.Commands
{
    public class StudentListCommand : AsyncCommand
    {
        public override Task<int> ExecuteAsync(CommandContext context)
        {
            AnsiConsole.MarkupLine($"[bold]List All Students[/]");
            return Task.FromResult(0);
        }
    }
}

Finally, create the StudentViewCommand class:

using Spectre.Console.Cli;
using Spectre.Console;
using System.ComponentModel;
using System.Threading.Tasks;

namespace Cli.Lessons.Commands
{
    public class StudentViewCommand : AsyncCommand<StudentViewCommand.Settings>
    {
        public class Settings : CommandSettings
        {
            [CommandArgument(0, "<ID>")]
            [Description("Id used to find student.")]
            public int Id { get; set; }
        }

        public override Task<int> ExecuteAsync(CommandContext context, Settings settings)
        {
            AnsiConsole.MarkupLine($"[bold]View Student =>[/] id[[{settings.Id}]]");
            return Task.FromResult(0);
        }
    }
}

These asynchronous commands are defined. Now, let’s see how we can configure them.

Configure the Commands

Async command configuration is exactly the same as that for synchronous commands. All of the same configuration methods, root commands, branches, etc. So there are only minor changes required to our Program.cs file to run those commands asynchronously. Update that file with the latest code:

using Cli.Lessons.Commands;
using Spectre.Console.Cli;
using System.Threading.Tasks;

namespace Cli.Lessons
{
    class Program
    {
        public static async Task<int> Main(string[] args)
        {
            var app = new CommandApp();
            app.Configure(config =>
            {
                config.CaseSensitivity(CaseSensitivity.None);
                config.SetApplicationName("lesson");
                config.ValidateExamples();

                config.AddBranch("student", student =>
                {
                    student.SetDescription("View, list, add or remove students.");

                    student.AddCommand<StudentAddCommand>("new")
                        .WithAlias("add")
                        .WithDescription("Add new student information.")
                        .WithExample(new[] { "student", "new", "1001", "Bill", "Shakespeare", "--enrollment", "5/14/1549" });

                    student.AddCommand<StudentViewCommand>("view")
                        .WithDescription("View student information by id.")
                        .WithExample(new[] { "student", "view", "1001" });

                    student.AddCommand<StudentListCommand>("list")
                        .WithDescription("View list of students.")
                        .WithExample(new[] { "student", "list" });

                    student.AddCommand<StudentDeleteCommand>("delete")
                        .WithAlias("del")
                        .WithDescription("Remove student from list.")
                        .WithExample(new[] { "student", "delete", "1001" });
                });
            });

            return await app.RunAsync(args).ConfigureAwait(false);
        }
    }
}

In line #9, we update the Main method signature to add the async keyword and return Task<int>, which also makes it an asynchronous method.

And line #42, we use the await keyword and run the CommandApp.RunAsync method. This runs the Spectre.Console.Cli.CommandApp and all of our commands asynchronously. That’s all it takes to make our console app run asynchronously.

Build and Test Async Commands

With all of the code in place, we can now build and run our asynchronous command line app. Let’s run some commands and see what they display. In these simple commands, there isn’t much visible difference because there isn’t any long running code…

1. Just the root command (to show the help text) – lesson student:

Fig 1 – Async Commands Help Text

2. Let’s add a student – lesson student new 1001 Darth Pedro:

Fig 2 – Async Add Command

3. Let’s try the other commands – lesson student delete 1001lesson student view 1001, and lesson student list:

Fig 3 – Remaining Async Commands

As we can see this behavior looks exactly like we built in lesson 1.3 as well.

In conclusion, we were able to change our commands to be asynchronous by deriving from AsyncCommand. We changed our Main function to also be asynchronous. The rest of the configuration code remained the same. Now we can use either sync or async commands depending on our requirements.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s