Build a Command Line Interface(CLI) Program with .NET Core

See the wikipedia.

Some great examples are:aws cliazure clidotnet cliangular cliWhy CLI?Programs with command-line interfaces are generally easier to automate via scripting.

Why .

NET Core?Obviously .

NET Core is not the only option to build a CLI.

And it is not even the most popular one at this moment for sure.

Python or Node.

js could be the better option if you are familiar with them.

But compare with .

NET, .

NET Core at least has met one of the most important requirements: cross platform.

Development environmentWindows 10Visual Studio 2017 Community Edition.

NET Core 2.

1ProgrammingCreate a new project choose [Console App (.

NET Core)]Edit Project File by right clicking the projectIn the project file, changetoLangVersion 7.

1 enables async main , you can also use latestRuntimeIdentifier enables publishing self-contained deployments.

AssemblyName specifies the output exe file name.

ConfigurationWe use the appsetting.

json file to save the configuration in .

NET Core.

Generic Host.

Net Core 2 introduced the Generic Host.

The host is responsible for app startup and lifetime management.

A very good document about the Generic Host here.

By using the generic host introduced in .

NET Core recent version, it will make it much easier to configure the logging, dependency injection etc.

LoggingSerilog is one of the most popular logging libraries for .

NET Core.

We use the appsetting.

json to configure the serilog .

ReadFrom.

Configuration(Configuration)Putting the configuration in the setting file rather than in the code makes it easier to change the logging behavior without modifying the code.

The configuration above uses a rolling file for each day.

Inside ConfigureServices, it enables the ILoggerinterface available for injection by providing the SerilogLoggerProvider as the Logging provider.

Dependency InjectionThe HostBuilder’s ConfigureServices passes the IServiceCollection, which can be used to configure the dependency injections.

For example,Command Line parserOne of the challenges of writing a CLI program is how to parse the command line.

We can always write the parser ourselves from scratch but this is too time consuming even though it is a very interesting task for a coder.

There are a couple of popular command line parser libraries available for .

NET Core:https://github.

com/dotnet/command-line-apihttps://github.

com/commandlineparser/commandlinehttps://github.

com/natemcmaster/CommandLineUtilsThe dotnet/command-line-api has a very interesting story behind.

And it is somehow related to natemcmaster/CommandLineUtils.

You can read the story here.

Hopefully it can be included in .

NET Core someday.

Read through it’s document is very helpful to develop a CLI program.

Also, understand how the windows command line involves is very helpful and interesting.

We eventually chose to use https://github.

com/natemcmaster/CommandLineUtils, We like it’sAPI syntax designDependency injection support with the generic hostSupport the async executionawait builder.

RunCommandLineApplicationAsync<iStradaCmd>(args);This line of code will use the class iStradaCmd as the main/root command to start the command line parser and execution with the dependency injection built in.

It works perfectly with the generic host.

There are three important concepts in this library: command, option and argument.

Those concepts came from the unix world Command line structureOption is a name/value pair, and argument is just value.

That simply means, option can show up in the command line in different places because the name can tell what option it is.

The argument , on the other hand, has no name, so it has to appear in a certain place in the command line.

The CommandLineUtils library supports the sub command.

For example,git commit -m “initial”git is the main/root command, commit is the sub command.

The CommandLineUtils library uses the attributes to map the command/sub command to class, and map the option and argument to class property.

This bridges between parse results and functionality.

A good example can be found here.

[https://github.

com/natemcmaster/CommandLineUtils/blob/master/docs/samples/subcommands/inheritance/Program.

cs]A complete code for the program class:Root CommandFor our CLI program, we I implement two sub commands for the first version.

istrada loginistrada list-ticketslogin will allow user login with their credentials.

The credentials will be saved locally as part of its profile so that user does not need to login again next time.

list-tickets will fetch the tickets from the web service.

We have class iStradaCmd for the main command, class LoginCmd for sub command login and class ListTicketCmd for sub command list-tickets.

Also I will have all my command classes inherited from the base class iStradaCmdBase.

That will be helpful since all the command class might want to have some common information like user profile and some common functions like http request/response processes.

Here is how class iStradaCmd looks like:The CommandLineUtils library’s attributes are pretty much self-explained.

Login CommandLogin will save the credentials locally in a file, also, it will verify the credentials using the istrada’s api once saved so the user is aware of if they entered the right credential information or not.

the credentials can be passed in through the option -u and -p, or if not presented, the program will ask the credentials input at run time.

Here is how class LoginCmd looks like:Command login has three options: username, password, and staging.

Those three options are mapped to properties on the class through the attributes.

In the method OnExecute, it checks if the username or password is passed in through the command line.

If not, it asks the user to input this information since those options are required in order for command to continue to run.

Class Prompt is a helper from the library that takes the input from console.

Prompt.

GetPasswordAsSecureString will display asterisks when user type password from the console.

It writes the user profile to a file under the user’s folder.

So next time, the program will load the profile from the file and user does not need to enter this information every time.

The password is encrypted when saved in the local file for security.

It then call the API to try to authenticate.

Login support multiple profiles.

You can login in as different user and specified a different profile name.

Multiple profiles will be saved locally.

Later, all other commands can pass the proflle option to specify which profile to use for that command execution.

If option profile not specified from the command line, it will default to profile ‘default’.

List Ticket CommandList Ticket command retrieves the tickets from istrada.

It accept start-date and end-date options to filter the tickets.

If those two options are not presented on the command line, it will prompt the user to enter those options.

It then format the url and call the iStrada API to retrieve the tickets.

The returned data is in json format.

It call OutputJson to output the data.

Currently iStrada API always return data in json format.

OutputJson is a method on the command base class.

The base classiStradaCmdBase will handle how to output the data.

FileNameSuffix is also a property on the base class.

FileNameSuffix will be used to format the filename’s suffix if output the data to a local file.

Base Command classAll the command classes are derived from the base class iStradaCmdBase .

The base class holdssome common options that applied for most commands, for example, Profile, OutputFormat, OutputFile, XSLTFile.

some protected properties that used by inherited command classes, for example, _logger, _httpClientFactory, _console and FileNameSuffix.

some common methods that are useful for all other inherited classes, for example Encrypt, Decrypt, OnExceptionHere is how the code looks like:When the class ListTicketCmd retrieved the tickets, it will calls OutputJsonOutputJson(tickets, “tickets”, “ticket”);This is because currently the iStrada API can only return data in JSON.

OutputJson is a method in the base class, the method will check option output-format, the option default to json, if the output-format is xml , it will convert the data to XML format using the Json.

NET library’s JsonConvert.

DeserializeXNode and then call method OutputXml.

In method OutputXml, it will check if the command line passed in option xslt, which points to a local XSL file.

If so, will use the XSL file to transform the XML data to another format (xml, html, csv, etc).

The reason we want to support the XSLT transformation is that every customer wants to export the data to their own specific format, even though they might use the same vendor accounting software.

By supporting the XSLT transformation, we don’t need to hardcode the data transformation in the code, rather, the transformation file is saved in a separate file that we can modify it without changing the program code.

We also pass a XSLTExtension object as the XSLT extention to the XSLT transformation.

As the .

NET only implemented XSLT 1.

0.

We can use the class XLSTExtension to implement some features XSLT 1.

0 does not support or hard to achieve.

The class XLSTExtension looks like this:After we transformed the data, now we can output the data.

We could output the data to the console as default, or if the command line passed the option output with a file path, we will save the data to the file.

Methods Encrypt and Decrypt provide the function to encrypt the password of the user profile.

It uses the AES (Advanced Encryption Standard) with the key related to the username of the OS.

So that, if you copy this file over to another PC or use it under another user, it wont work.

This improves the security, but It is not a good enough practice strictly speaking.

iStradaClientiStradaClient is just the wrapper class of iStrada RESTful API.

The HttpClient passed in is created by _httpClientFactory that injected in by the dependency injection from the command class.

Here explains why .

AutomateWith the CLI tool in place, use a script to automate this process becomes a relatively easy task.

For example, on windows platform, we can write a PowerShell script to automate this process if customer wants to run this task daily to extract the ticket and save to a local file automatically.

And then use the windows scheduler to setup the schedule to run it.

Here is an exampleDistributionWhen a lot of users use the CLI tool, one challenge will be how to update the software to the most recent version.

Squirrel.

Windows is a great .

NET solution for the windows desktop software distribution.

It handles the installation and update seamlessly.

https://github.

com/Squirrel/Squirrel.

WindowsAfterthoughtIt is a very simple program.

But it does indicate the necessary parts to build a CLI program.

As a developer, CLI is more developer friendly than the rich GUI.

When we manage our cloud solution on AWS or Azure, or use some framework like .

NET Core or Angular 2+, we try to use the command line interface as much as we can to avoid using the more user friendly portal.

Those experiences will definitely help us understand how a well-designed CLI program works and some important conventions of the syntax design.

.

. More details

Leave a Reply