.NET Core Global Tools - (What are Global Tools, How to Create and Use them)

Posted by: Damir Arh , on 5/13/2019, in Category .NET Standard & .NET Core
Views: 45653
Abstract: The tutorial provides an overview of .NET Core Global tools: how to use them, how to create them and what to expect of them in future versions of .NET Core.

What are Global Tools?

.NET Core Global Tools were introduced as part of .NET Core 2.1.

In essence, they are console applications, i.e. they are meant to be used from the command line, or from scripts. Their main distinguishing feature is that they are distributed as NuGet packages and that the .NET Core SDK includes all the necessary tooling for installing, running, updating and uninstalling them.

This concept might sound familiar to readers who are also JavaScript developers. The idea is very similar to globally installed npm (Node.js package manager) packages that are designed to be used as command line tools, e.g. Apache Cordova, Angular CLI, Vue CLI, etc.

Since global tools are distributed as NuGet packages, they are published in the official NuGet Gallery just like any other NuGet package containing a class library. Unfortunately, there’s currently no way to restrict the gallery search to only global tools, which makes it difficult to find them and to recognize them in the search results.

At the time of writing this article, the best available method for discovering global tools was a list maintained by Nate McMaster. It is implemented as a GitHub repository and new entries can be easily contributed to it as pull requests or issues. The maintainer seems to be doing a good job at processing new submissions quickly.

Global tools primarily target developers.

That’s the reason why most of the tools in the list provide functionalities which are useful in some part of the development process. To give you an idea of what to expect or what to look for, here’s a short selection of the tools from the global tools list:

  • dotnet-ignore can download a .gitignore file of your choice from the repository of templates for different types of projects. It is most useful when you are creating a new project to get you started with an initial .gitignore file before you make your first commit to a Git repository. If your IDE or text editor doesn’t do that for you already, it can make your life much easier.
  • dotnet-serve is a simple web server for static files in your current folder. It allows you to open local files in the browser via the HTTP protocol. This way you will avoid restrictions that the browsers impose on JavaScript files loaded from the local file system (e.g. local JavaScript files aren’t allowed to load other resources dynamically).
  • dotnet-cleanup cleans up a project folder, i.e. it deletes files that were download by a package manager or generated during build, e.g. files in bin and obj folders.
  • dotnet-warp is a wrapper tool around Warp, simplifying its use for .NET Core projects. It can be used to generate stand-alone executable files for applications developed in .NET Core.
  • Amazon.ECS.Tools, Amazon.ElasticBeanstalk.Tools and Amazon.Lambda.Tools are official global tools from AWS (Amazon Web Services) to make it easier to deploy applications to Amazon Elastic Container Service, AWS Elastic Beanstalk and AWS Lambda, respectively.

This is by no means an exhaustive list. I encourage you to look at the full list on GitHub and see if it contains any tools that could improve your development process. It’s not a long read, so you should be ok.

When deciding to install a global tool, keep in mind that you will be running it outside a sandboxed environment with the same permissions as you have.

Therefore, you will have to always trust the tool you are about to install. Seeing the tool in the above-mentioned list should make it more legitimate. Nevertheless, it would still be prudent to always check the package download numbers in NuGet gallery and any other packages published by the same author, before installing a global tool.

Most, if not all, global tools are open source, so you can also check their source code to be extra safe.

Managing and using global tools

The only prerequisite for using .NET Core global tools is having .NET Core SDK 2.1 or newer installed. If a global tool targets a newer version of .NET Core, you need to have a compatible version of the runtime installed.

The management of global tools on the local machine is done using the dotnet tool command. To install a global tool, its NuGet package name must be specified, e.g.:

dotnet tool install -g dotnet-ignore

The above command always installs the latest stable version of the package. If you want to install a specific version (an older one or a prerelease one), you must explicitly specify the version:

dotnet tool install -g dotnet-ignore --version 1.0.1

The install process puts the tool in a folder inside your profile (the .dotnet/tools subfolder to be exact). The folder is automatically added to the PATH user environment variable. This makes it specific to your user account on the machine, but available from any folder without having to specify the path to it.

The name of the command does not necessarily match the name of the NuGet package (although this is often the case). The name of the command to invoke is printed out when you install it, e.g.:

You can invoke the tool using the following command: dotnet-ignore
Tool 'dotnet-ignore' (version '1.0.3') was successfully installed.

You can always get a list of all the global tools installed and their corresponding commands by invoking the dotnet tool list command:

> dotnet tool list -g
Package Id         Version      Commands
---------------------------------------------
dotnet-ignore      1.0.3        dotnet-ignore

You can control the behavior of commands with command line arguments. Each command has its own set of supported arguments. You can usually list them if you invoke the command with the -h option, e.g.:

dotnet-ignore -h

The output should be enough for you to learn how to use a command. Some global tools have more detailed documentation published on their NuGet page or on their project site (the NuGet package always includes a link to it).

dotnet-serve-nuget-page

Figure 1: NuGet page of dotnet-serve global tool

In the case of dotnet-ignore, two subcommands are available:

- List prints out a list of all available .gitignore files:

dotnet-ignore list

- Get downloads the specified .gitignore file:

dotnet-ignore get -n VisualStudio

You can use the dotnet tool command to perform several other actions:

- List all global tools installed for your user account (the list includes package name, version and command to invoke):

dotnet tool list -g

- Update a global tool to its latest stable version, e.g.:

dotnet tool update -g dotnet-ignore

- Uninstall a global tool from your user account, e.g.:

dotnet tool uninstall -g dotnet-ignore

To install a different version (not the latest stable one) of an already installed global tool, you must uninstall it first, and then install the selected version, e.g.:

dotnet tool uninstall -g dotnet-ignore
dotnet tool install -g dotnet-ignore --version 1.0.1

Developing your own global tool

As already mentioned, global tools are .NET Core console applications. Therefore, to create one, you need to select the Console App (.NET Core) template when creating a new project in Visual Studio. You can use the dotnet new console command to create a suitable project from the command line.

The project generated from the template is already a very simple global tool which prints out “Hello World!” when you invoke it:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Of course, you will want to affect the behavior of your global tool with command line arguments. If you need to support multiple subcommands, each with several options, argument parsing can quickly become a complex task. To make your life easier, you should use a class library to handle that and focus on the core functionality of the global tool instead.

A good choice would be CommandLineUtils class library which is used by many other global tools as well. To use it, you need to install the McMaster.Extensions.CommandLineUtils NuGet package in your project.

You can then implement each subcommand as a separate class with a Command attribute. In the following example, the Print command will print out the contents of a file to the console:

[Command(Description = "Prints out the contents of a file.")]
class Print
{
    // ...
}

Options are defined as properties with an Option attribute. Data annotation attributes can be used to implement value validation:

[Required]
[Option(Description = "Required. File path.")]
public string Path { get; }

The OnExecute method should contain the actual command. The injected instance of IConsole should be used to output text to console:

public void OnExecute(IConsole console)
{
    try
    {
        var contents = File.ReadAllText(Path);
        console.WriteLine(contents);
    }
    catch (Exception e)
    {
        console.WriteLine(e.Message);
    }
}

A command for listing the contents of a folder can be implemented in a similar manner:

[Command(Description = "Lists files and folders.")]
class List
{
    [Option(Description = "Folder path.")]
    public string Path { get; } = ".";

    public void OnExecute(IConsole console)
    {
        try
        {
            var dir = new DirectoryInfo(Path);

            foreach (var subdir in dir.GetDirectories())
            {
                console.WriteLine($"{subdir.Name} (DIR)");
            }

            foreach (var file in dir.GetFiles())
            {
                console.WriteLine($"{file.Name}");
            }
        }
        catch (Exception e)
        {
            console.WriteLine(e.Message);
        }
    }
}

All supported subcommands must be declared with the Subcommand attribute of the Program class:

[Command(Description = "Text file printer")]
[Subcommand(typeof(List), typeof(Print))]
class Program
{
    // ...
}

Since the Program class is also a command, it requires its own OnExecute method which will be invoked when no subcommand command line argument is specified. This sample global tool will simply notify the user that a subcommand must be specified, and print out the help text:

public int OnExecute(CommandLineApplication app, IConsole console)
{
    console.WriteLine("You must specify a subcommand.");
    console.WriteLine();
    app.ShowHelp();
    return 1;
}

Notice that this OnExecute method returns a non-zero return value which by convention indicates an error condition. The OnExecute methods with void return type are treated as if they always returned 0. This matches the behavior of the Main method in a console application.

In the Main method, the input arguments must simply be relayed to the class library:

static int Main(string[] args)
{
    return CommandLineApplication.Execute<Program>(args);
}

For the global tool to return an exit code, the Main method must return the value returned by the CommandLineApplication.Execute static method which will match the return value of the OnExecute method invoked.

With almost no plumbing code, we have created a fully functional command line tool. If we invoke it with no arguments, the autogenerated help text will be displayed:

> dotnet-fs
You must specify a subcommand.

Text file printer

Usage: dotnet-fs [options] [command]

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

Commands:
  list          Lists files and folders.
  print         Prints out contents of a file.

Run 'dotnet-fs [command] --help' for more information about a command.

Help text is also autogenerated for each subcommand:

> dotnet-fs print -h
Prints out contents of a file.

Usage: dotnet-fs print [options]

Options:
  -p|--path <PATH>  Required. File path.
  -?|-h|--help      Show help information

Missing required parameters are correctly handled:

> dotnet-fs print
The --path field is required.
Specify --help for a list of available options and commands.

Of course, the core functionality of printing out the file contents works as well:

> dotnet-fs print -p .\bin\Debug\netcoreapp2.2\dotnet-fs.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "netcoreapp2.2",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "2.2.0"
    }
  }
}

This example is only a small part of what the CommandLineUtils class library can do for you in a console application. To learn more, you should read the official documentation.

Publishing your global tool

To distribute the global tool (and also to install it locally), it must be packed into a NuGet package. If you’re using Visual Studio, you can fill out most of the NuGet package metadata on the Package pane of the project Properties window.

nuget-package-metadata

Figure 2: NuGet package metadata

Unfortunately, Visual Studio does not yet have any user interface for setting the most important NuGet configuration option. So to do that, the .csproj project file must be edited manually.

Right-click the project in the Solution Explorer window and click the Edit <project-name>.csproj menu item to open the project file in the editor window. Add the <PackAsTool> element to the <PropertyGroup> element which already contains all the other NuGet metadata:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.2</TargetFramework>
  <PackAsTool>true</PackAsTool>
  <RootNamespace>dotnet_fs</RootNamespace>
  <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  <Authors>Damir Arh</Authors>
  <Company />
  <Description>.NET Core global tool for printing file contents to console</Description>
  <PackageProjectUrl>https://github.com/damirarh/dotnet-fs</PackageProjectUrl>
  <RepositoryUrl>https://github.com/damirarh/dotnet-fs</RepositoryUrl>
  <PackageLicenseUrl>https://github.com/damirarh/dotnet-fs/blob/master/LICENSE</PackageLicenseUrl>
  <RepositoryType></RepositoryType>
</PropertyGroup>

If you checked the Generate NuGet package on build checkbox in the Properties window, the NuGet package will be generated automatically in the bin\Debug folder when you build the project the next time.

To test the global tool before publishing it to the NuGet Gallery, you can use the --add-source option of the dotnet tool install command to instruct it with the location where the NuGet package with the global tool can be found:

dotnet tool install --add-source .\bin\Debug --tool-path tools dotnet-fs

I also used the --tool-path option to install the tool into the tools subfolder, instead of installing it globally. This option allows you to specify a folder (relative or absolute) where the tool will be installed, instead of in the default folder (i.e. .dotnet/tools inside your profile). It can be used with all the other dotnet tool commands as well.

This destination folder is not automatically added to your PATH user environment variable. So, to test your command, you must specify the full path to it:

.\tools\dotnet-fs.exe

Once you’re satisfied with your new global tool, you can publish it to the NuGet Gallery like any other NuGet package so that others will be able to use it as well. You should also consider submitting a pull request to the global tools list repository to add your tool to the list, and increase its discoverability.

Upcoming changes in .NET Core 3.0

In .NET Core 3.0, global tools will be expanded with additional support for locally installed tools specific to a code repository. When such tools are required by the code in the repository (e.g. during the build process), this approach has the following advantages over globally installed tools:

· A specific version of a tool can be installed for the repository, independently of the globally installed version of the same tool. This can ensure predictable and reproducible behavior even when there are breaking changes between versions of the tool.

· The tools required by the code in the repository are listed in a manifest file which is included in the repository. When a new developer retrieves the code from the repository, she/he can easily set up the environment by restoring the tools listed in the manifest file.

This local-tools scenario should be familiar to JavaScript developers just like the global-tools one. When npm packages are installed locally, they can be added to the package.json manifest as development dependencies. They are then restored along with all the other dependencies and can be used from scripts which are also defined in the same package.json file.

Initial support for local tools is included in the currently available .NET Core 3.0 Preview 2. Some implementation details can still change before the final release, although this doesn’t prevent you from trying out the feature if you have .NET Core SDK 3.0 Preview 2 installed.

Just don’t rely on it yet for your projects!

Before installing any tools locally, you should first create the manifest file in the root of your repository using the dotnet new command:

dotnet new tool-manifest

This will create an empty .config\dotnet-tools.json manifest file with the following content:

{
  "version": 1,
  "isRoot": true,
  "tools": {}
}

You can now install a tool locally by invoking the dotnet tool install command without the -g option:

dotnet tool install dotnet-serve

A different syntax must be used for invoking locally installed tools. This is explained to you when the installation succeeds:

You can invoke the tool from this directory using the following command: dotnet tool run dotnet-serve
Tool 'dotnet-serve' (version '1.1.0') was successfully installed. Entry is added to the manifest file D:\Temp\local-tools-sample\.config\dotnet-tools.json.

Using this syntax, the command can be invoked from any folder (directly or indirectly) inside the one containing the manifest file in the .config subfolder. This usually makes it a good idea to create the manifest file in the root folder of the repository.

If you check the previously created manifest file, you can also see the entry for the locally installed tool as mentioned above:

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-serve": {
      "version": "1.1.0",
      "commands": [
        "dotnet-serve"
      ]
    }
  }
}

There’s no need to check the manifest file to see which local tools are installed. The dotnet tool list command can be used for that instead:

> dotnet tool list
Package Id        Version      Commands          Manifest
-----------------------------------------------------------------------------------------------------------
dotnet-serve      1.1.0        dotnet-serve      D:\Temp\local-tools-sample\.config\dotnet-tools.json

The command will list the same tools when another developer downloads a repository with this manifest file. If she/he tries to immediately invoke the tool, she/he will be instructed to run the dotnet tool restore command first:

> dotnet tool run dotnet-serve
Run "dotnet tool restore" to make the "dotnet-serve" command available.

After running the command as instructed, the dotnet serve command will become available as well:

> dotnet tool restore
Tool 'dotnet-serve' (version '1.1.0') was restored. Available commands: dotnet-serve

Restore was successful.
> dotnet tool run dotnet-serve
Starting server, serving .
Listening on:
  http://localhost:8080

Press CTRL+C to exit

Despite their name, local tools aren’t installed inside the repository folder. When invoked using the dotnet tool run command, they are run from the shared global NuGet packages folder (by default, the .nuget folder inside the user profile). This means that multiple repositories with the same version of a local tool installed will share the same install location. Also, the dotnet tool restore command might not even be necessary if the developer already has the required tools because they are also used by other repositories.

Conclusion

.NET Core global tools are a useful addition to the .NET Core ecosystem. They provide an easy way for distributing and deploying command line tools.

As more developers become familiar with them, the selection of tools available will also grow. As I have shown, creating a command line tool is not a daunting task if you take advantage of available class libraries.

.NET Core 3.0 will support even more use cases with the addition of local tools. Having the required tools specified inside a code repository for a project, simplifies the process of ensuring a working environment for all developers. This could encourage developers to make projects dependent on specific local tools.

This article was technically reviewed by Daniel Jimenez Garcia.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!