Building and using advanced .NET Core CLI global tools
Image from Pexels by cottonbro

Building and using advanced .NET Core CLI global tools

Handling commands, arguments and options in .NET CLI global tool application

Once .NET Core 2.1 SDK introduced support for global tools I wrote a brief article on how to create a simple global CLI tool in .NET Core Building advanced .NET Core Global Tool using CommandLineUtils package.

While this is fair enough informations to kick-start you project and create a simple global tool to use from the command line, I found out there is more to it in order to have the tool built, published, distributed and used especially when you are dealing with real-life applications that need to have a CLI as a part of the solution.

In recently published article Seeding data in EF Core using SQL scripts on how to use SQL scripts for data seeding, I realized that there are two potential steps that can cause human error for the simple reason they are not part of everyday routine while developing and seeding itself is not that frequent operation.

These two steps are:

  • creating seeding SQL script file with a proper name which starts with current date and time in a specific format (yyyyMMddHHmmss)
  • including created file as and embedded resource in .csproj file

For this reason I decided to automate this and made it more familiar to developers who work on the project. The idea is to similarly to migrations, you can invoke a tool from command line which will create SQL script seeding file with a proper name and include it as an embedded resource in .csproj file. If you want to get your hands on the code straigh away you can clone sample-seeding-sql repository and dive into Sample.Seedind.Tool project. However, there is more in this article related not only to code but as well as installing, using and distributing your global tool.

Setting up the global CLI tool project and debugging it

.NET global tool is nothing but a simple Console Application Project which has handles argument in a bit more advanced way. Therefore initial setup is pretty much straight forward. You need to create new Console Application Project and add reference to Microsoft.Extensions.CommandLineUtils NuGet package which will help you handle the arguments from the command line.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
  </ItemGroup>
</Project>
    

We are ready now to dig into code and see how are we going to handle values passed to our CLI global tool. Before you start analyzing the code snippet below, you need to understand the elements that we are going to handle in our code and which are passed to the application from command line.

We are going to handle following types of elements that can be passed to our code from the command line to handle it:

  • Commands - you may have multiple routines that your application/tool can handle for example add/remove
  • Arguments - values that are passed to a command in a specific order since values are not identified with any flag
  • Options - vallues identified with a flag so their order in a command line is not important 

As a real life example I will use the code from sample-seeding-sql repository which helps adding seeding SQL script files to a project. For simplicity I excluded logic of the tool so we can focus only on handling command line params. You can wind complete code on GitHub.

using Microsoft.Extensions.CommandLineUtils;
using System;

namespace Sample.Seeding.Tool
{
    class Program
    {
        static void Main(string[] args)
        {
            var cmd = new CommandLineApplication();
            cmd.HelpOption("-? | -h | --help");

            cmd.Command("add", (cmd) =>
            {
                var argName = cmd.Argument("name", "Script name", false);
                var optName = cmd.Option("-n | --name <value>", "Script name", CommandOptionType.SingleValue);
                var optProject = cmd.Option("-p | --project <value>", "Project file path or project name", CommandOptionType.SingleValue);
                var optOutput = cmd.Option("-o | --output <value>", "Output folder path", CommandOptionType.SingleValue);
                cmd.HelpOption("-? | -h | --help");
                cmd.OnExecute(() =>
                {
                    // TODO: Execute command logic  

                    Console.WriteLine($"Argument name: {argName.Value}");
                    Console.WriteLine($"Option name: {optName.Value()}");
                    Console.WriteLine($"Option project: {optProject.Value()}");
                    Console.WriteLine($"Option output: {optOutput.Value()}");


                    Console.WriteLine($"Command add successfully performed");
                    return 0;
                });
            });

            cmd.Execute(args);
        }
    }
}


    

Let's analyze the code snippet above to better understand what are we doing there. First thing you maybe noticed is repetition of cmd.HelpOption("-? | -h | --help"); line. The reason for this is that we have two level of help documentation.

This can be simple explained if we run our code from Visual Studio. If you try to run the code now from Visual Studio without any adjustment of the command line parameters you will see that your command block for command "add" is not executed and there is no message of successful command completion.

To add command line parameters from Visual Studio you need to open properties page of a project and go to "Debug" tab on the left pane.

Dtonet Glob Debug Params

This can be also done by updating the Properties/launchSettings.json file directly

{
  "profiles": {
    "Sample.Seeding.Tool": {
      "commandName": "Project",
      "commandLineArgs": "add Sample -o Seedings"
    }
  }
}
    

Now let's test help options for the tool and for the "add" command which we have declared in the Main method of Program class. We'll first run the tool in a debug mode with -h option to see the output. First we need to update Properties/launchSettings.json

{
  "profiles": {
    "Sample.Seeding.Tool": {
      "commandName": "Project",
      "commandLineArgs": "-h"
    }
  }
}
    

The output will only describe the commands you can use without it's arguments or options

Usage:  [options] [command]

Options:
  -? | -h | --help  Show help information

Commands:
  add

Use " [command] --help" for more information about a command.

Now when we add the command name to a the command line parameters output will be different. It now includes all options and basically the syntax for using "add" command of our tool

{
  "profiles": {
    "Sample.Seeding.Tool": {
      "commandName": "Project",
      "commandLineArgs": "add -h"
    }
  }
}
    

The output will be as following

Usage:  add [arguments] [options]

Arguments:
  name  Script name

Options:
  -n | --name <value>     Script name
  -p | --project <value>  Project file path or project name
  -o | --output <value>   Output folder path
  -? | -h | --help        Show help information

You can use Properties/launchSettings.json file to control all aspects of you tool and manage to debug it before you have it ready for packaging and deploying it to NuGet package repository. If you want to know a bit more about how to pack and build NuGet packages to Azure DevOps you may find some references in article Stop writing clients in C# for your Web APIs where part of article explains how to automate build/pack/publish you NuGet packages to a private organization NuGet package repository using Azure DevOps pipeline.

In this article, to keep things simple, we'll install global tool from our machine file system, but there are few things to be taken care of before we actually pack our application to .nuspec package file.

Building NuGet package for the CLI global tool

Alright, we managed to implement the logic for our tool, we did some debugging and testing from Visual Studio IDE and now it's the time to create NuGet package, install it and try it out the way others will use it.

Microsoft has a nice piece of documentation where it explains some rules you need to follow in order to have your global tool being able to install and run. Although the title Troubleshoot .NET Core tool usage issues is not really intuitive, it contains a lot of explanations and rules you need to follow in order to have your global tool ready for distribution.

First and most important rule is the name of the result assembly which must be dotnet-<toolName>.exe. This means you need to adjust your output assembly name in the project file directly.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- What to build -->
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>

    <!-- Output .exe file name (needs to follow global tool naming) -->
    <AssemblyName>dotnet-seeding</AssemblyName>
    
    <!-- Package related properties -->
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>seeding</ToolCommandName>
    <PackageType>.NET CLI global tool</PackageType>
    
    <!-- Generate NuGet package (.nupkg) file in output (bin) folder on build -->
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>

  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
  </ItemGroup>
</Project>
    

Element AssemblyName now overrides the project name and will produce dotnet-seeding.exe file on build. Elements PackAsTool and ToolCommandName are also mandatory and need to be set in the .csproj file. Value of ToolCommandName is the actual tool name that you will you use to invoke your tool.

Element PackageType is not mandatory and it represents a short description of the type of the NuGet package. GeneratePackageOnBuild is used in order to generate .nuspec package file on build in addition to binaries that are produced during the build. Quite useful on local machine since you do not have invoke dotnet pack explicitly after dotnet build.

Now you can build your project from the Visual Studio and you will have .nuspec file created in project's /bin folder.

If you are curious to see how does your package actually looks like and what is all included in the package, I recommend that you install NuGet Package Explorer which will allow you to browse the package content.

 Package Explorer

Installing and using CLI global tool

Since I am focusing on building and packing global tool, I will not publish this tool NuGet package to public or Azure DevOps private NuGet package repository. Instead I will install global tool from the bin folder where .nuspec package file is generated on project build.

To install global tool from local file system you need to use --add-source option for install command of tool CLI tool which comes with .NET Core SDK. Make sure you execute the following command from the bin folder.

dotnet tool install -g --add-source ./ dotnet-seeding
    

Once you run the following command you should get confirmation that the tool is successfully installed

PS C:\Work\GitHub\sample-seeding-sql\Sample.Seeding.Tool\bin\Debug> dotnet tool install -g --add-source ./ dotnet-seeding
You can invoke the tool using the following command: seeding
Tool 'dotnet-seeding' (version '1.0.0') was successfully installed.

Now you can invoke the tool with same argument and option values using ToolCommandName which is defined in .csproj file

PS C:\Work\GitHub\sample-seeding-sql\Sample.Seeding.Tool\bin\Debug> seeding add ScriptName -o SeedingFolder -p MyProject.csproj
Argument name: ScriptName
Option name:
Option project: MyProject.csproj
Option output: SeedingFolder
Command add successfully performed

In case you need to do a bug fixing and you need to uninstall the tool in order to install it again, it is even simpler that installing it

dotnet tool uninstall -g dotnet-seeding
    
PS C:\Work\GitHub\sample-seeding-sql\Sample.Seeding.Tool\bin\Debug> dotnet tool uninstall -g dotnet-seeding
Tool 'dotnet-seeding' (version '1.0.0') was successfully uninstalled.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article