blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Running one-off .NET tools with dnx

Exploring the .NET 10 preview - Part 5

Share on:

In this post I briefly show the new dnx command for running one-off .NET tools without installing them. I show how to use the command, how the command works in practice, and how the command works behind the scenes in the .NET SDK.

Running tools without installing them with dnx

The Node.js ecosystem has had a tool called npx since 2017. Sitting alongside the npm package-manager tool, it allows running a Node.js tool from a package without having to installing it globally. .NET Core added support for tools in NuGet packages shortly afterwards in 2018, back in .NET Core 2.1, but this has always required an explicit dotnet tool install command before you can run the tool. Until now.

.NET 10 preview 6 added support for running .NET tools without explicitly installing them first with the introduction of the new .NET SDK command dnx. What's more, the .NET SDK also ships a standalone dnx command so that you can run dnx <tool> directly instead of dotnet dnx <tool>.

One of the easiest ways to understand what's available with the new dnx command is to take a look at the built-in command line help:

> dnx --help
Description:
  Executes a tool from source without permanently installing it.

Usage:
  dotnet dnx <packageId> [<commandArguments>...] [options]

Arguments:
  <PACKAGE_ID>        Package reference in the form of a package identifier like 'Newtonsoft.Json' or package identifier and version separated by '@' like '[email protected]'.
  <commandArguments>  Arguments forwarded to the tool

Options:
  --version <VERSION>       The version of the tool package to install.
  -y, --yes                 Accept all confirmation prompts using "yes."
  --interactive             Allows the command to stop and wait for user input or action (for example to complete authentication). [default: True]
  --allow-roll-forward      Allow a .NET tool to roll forward to newer versions of the .NET runtime if the runtime it targets isn't installed.
  --prerelease              Include pre-release packages.
  --configfile <FILE>       The NuGet configuration file to use.
  --source <SOURCE>         Replace all NuGet package sources to use during installation with these.
  --add-source <ADDSOURCE>  Add an additional NuGet package source to use during installation.
  --disable-parallel        Prevent restoring multiple projects in parallel.
  --ignore-failed-sources   Treat package source failures as warnings.
  --no-http-cache           Do not cache packages and http requests.
  -v, --verbosity <LEVEL>   Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].
  -?, -h, --help            Show command line help.

As you might expect, there's a lot of overlap with the existing dotnet tool commands. One point that differs is the (optional) way you can specify the package version with a @ separator, for example, [email protected]. This is a common pattern used in other ecosystems like Node.js and Go, for example, and it's now making it's way to .NET.

I think we first saw this notation in .NET with the recent dotnet run app.cs feature. Overall, it points to a willingness to break from some of .NET's tendency towards verbosity in favour of embracing more concise patterns, particularly patterns from other ecosystems.

So now let's try it out. As an example, I'll use the classic demo tool dotnetsay. When you run using dnx dotnetsay, you have to confirm that the .NET SDK should download the tool. If you answer n, the run is cancelled; if you answer y then it downloads and runs the tool:

> dnx dotnetsay
Tool package [email protected] will be downloaded from source https://api.nuget.org/v3/index.json.
Proceed? [y/n] (y): y

        Welcome to using a .NET Core global tool!

Note that you only get the confirmation the first time you run and download the tool. The next time, the tool runs without confirmation:

> dnx dotnetsay

        Welcome to using a .NET Core global tool!

And there you have it: one-shot .NET tool execution.

How is dnx different to dotnet tool install?

Now, you might be thinking that this dnx is nice and short, but is it really that different to installing the tool? And the answer is both yes and no.

The typical way you would install the dotnetsay tool globally would be using:

dotnet tool install -g dotnetsay

The -g means the tool is installed globally, though you can also install tools locally. You can then run the tool by simply running:

dotnetsay

        Welcome to using a .NET Core global tool!

But what does "installed globally" actually mean? And how does it differ from what dnx does?

Step one in both cases is to download the dotnetsay NuGet package. This goes through layers of caching and is ultimately expanded into the global package location. This is also where all the NuGet packages that are downloaded as part of project builds are stored by default.

You can see the location of the NuGet-related folders on your machine by running dotnet nuget locals all --list

After downloading the package, the SDK also copies the expanded package to the "dotnet tool" store location. By default, these expanded directories can be found at ~/.dotnet/tools/.store. The .NET SDK also installs an executable shim in ~/.dotnet/tools, which is also on the machine's PATH, so they can be easily invoked.

In contrast, the dnx command downloads the package and installs it into the global package cache, but it doesn't install anything in the tool store or add a shim to ~/.dotnet/tools. Instead it runs the tool directly from the package cache. This is what makes it the "one shot" run instead of the persistent dotnet tool install approach.

How does dnx work behind the scenes?

Whenever a big new feature drops in .NET, I like to sniff around to see how it was implemented, and dnx was no different. I started by finding the origin of the dnx command itself by running where dnx from a command line:

> where dnx
C:\Program Files\dotnet\dnx.cmd

As you can see, the dnx command is actually a cmd file, installed side-by-side with the dotnet.exe. If you crack it open you can see that all it's doing is invoking dotnet dnx, and passing the remaining arguments:

@echo off
"%~dp0dotnet.exe" dnx %*

There's a similar file for Linux and MacOS that provides the dnx command, and as expected, it does a similar thing:

#!/bin/sh

# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the MIT license.

"$(dirname "$0")/dotnet" dnx "$@"

So the .NET SDK exposes a dnx command which parses the arguments and options, and creates an instance of the ToolExecuteCommand. This command is responsible for downloading and running the command.

At a high level, this command does the following things:

  1. If a version for the tool is not specified, try to locate the tool in the local tools manifest. If it's present in the manifest, run the tool from there.
  2. Otherwise, try to find the package to install from nuget.org.
  3. If the tool is not currently downloaded, ask for permission and download the tool.
  4. Finally, run the tool.

That's pretty much all there is to it, but I'll go into a little more detail below.

The ToolExecuteCommand only considers the local tool manifest if you don't specify a version to install, otherwise it skips the step entirely. Next the command looks for a tool manifest, dotnet-tools.json. If it finds a manifest, and the tool being run is listed in the manifest, the command downloads and runs the tool without any further interaction.

If you do specify a version, or if there's no manifest, or the manifest doesn't list the tool being run, then it moves onto installing the tool globally. First it searches for the package on nuget.org, or whichever sources are configured in the nuget.config file.

Assuming the requested package exists, the command next checks whether it has permission to download the package. The command prompts for confirmation as we saw earlier (Proceed? [y/n] (y): y). Interestingly, this prompt is shown whether or not you're running in an interactive setting; though you can bypass the flag by passing the auto-confirm --yes flag.

Once the command has confirmation, it downloads the tool and runs it. Voila!

That's about all there is to the dnx command. It's a nice little quality of life bump for the .NET ecosystem, which leverages all the existing features of .NET tools to make things that little bit easier.

Summary

In this short post I showed the new dnx command and how to use it to run .NET tools without explicitly installing them first. Next I discussed the difference between the dnx command and the dotnet tool install command. Finally I walked through the code behind the feature; the ToolExecuteCommand in the .NET SDK.

  • Buy Me A Coffee
  • Donate with PayPal
Andrew Lock | .Net Escapades
Want an email when
there's new posts?