In .NET 10, the CLI picked up a trio of quality‑of‑life upgrades that remove friction from everyday dev and CI workflows:
- Ad‑hoc tool execution so you can run a .NET tool without installing it.
- A new dnx runner that makes tool invocation dead simple.
- Big enhancements to file‑based apps, including publish and native AOT.
In this post, I’ll show why these matter, how they work, and when to reach for each using concise, copy‑pasteable examples you can drop into your Windows PowerShell or CI scripts.
Why you should care
- Less bootstrapping: Run tools in CI without pre‑install steps or global state.
- Faster feedback: File‑based apps let you script in C# with instant runs and native AOT publish.
- Cleaner ergonomics: dnx shortens commands and forwards arguments as‑is to the CLI.
Ad‑hoc tool execution (no install required)
You can now execute a .NET tool straight from NuGet without installing it globally or locally.
- Command:
dotnet tool exec <PackageId>[@version] [args…] - Uses the tool version in a nearby
.config/dotnet-tools.jsonif present; otherwise the latest version unless you pin it (for example,toolsay@1.0.0). - Prompts before first download (interactive terminals) and caches thereafter.
Example with a local feed source and visible prompt:
# Run the tool once without installing it
dotnet tool exec --source .\artifacts\package\ toolsay "Hello, World!"
Sample output (from docs):
Tool package toolsay@1.0.0 will be downloaded from source <source>.
Proceed? [y/n] (y): y
_ _ _ _ __ __ _ _ _
| | | | ___ | | | | ___ \ \ / / ___ _ __ | | __| | | |
| |_| | / _ \ | | | | / _ \ \ \ /\ / / / _ \ | '__| | | / _` | | |
| _ | | __/ | | | | | (_) | _ \ V V / | (_) | | | | | | (_| | |_|
|_| |_| \___| |_| |_| \___/ ( ) \_/\_/ \___/ |_| |_| \__,_| (_)
|/
Tips
- CI friendly: add
--non-interactiveto suppress prompts, or pre-restore a manifest (dotnet tool restore). - Trust the source: use
--sourcefor your internal feed; prefer pinning versions for reproducibility.
The new dnx tool runner
There’s a new dnx CLI command that forwards args to the dotnet CLI for tool execution. It’s the simplest way to “just run a tool”. Think of it as npx, but for .NET tools.
# As simple as it gets
dnx toolsay "Hello, World!"
Notes
dnxis implemented by thedotnetCLI; behavior evolves with the SDK.- It’s great for quick local runs and scripting; for deterministic CI, prefer
dotnet tool execwith explicit sources and versions.
When to use which
- Use
dnxwhen you want the shortest possible command and you’re okay with the default resolution behavior. - Use
dotnet tool execwhen you need control (version pinning, feed selection, non‑interactive behavior) or you’re running in CI/CD. - Use a local tool manifest (
dotnet new tool-manifest+dotnet tool install) when your repo standardizes tool versions for all devs.
File‑based apps, level‑up in .NET 10
File‑based apps let you put a complete program in a single *.cs file and run it directly. In .NET 10, they gained publish support and default to native AOT, so your tiny scripts can ship as tiny native executables.
Run a single‑file C# app
// hello.cs
Console.WriteLine("Hello, file-based app!");
# compile+run in one shot
dotnet run .\hello.cs
# Output:
# Hello, file-based app!
Publish (native AOT by default)
# Produce a native executable for Windows x64
dotnet publish .\hello.cs -r win-x64 -c Release -o .\publish
# Result: .\publish\hello.exe (native AOT)
Need to turn AOT off (for example, when using packages or reflection features that aren’t AOT-friendly)? Add a property directive at the top of your file:
#:property PublishAot=false
Console.WriteLine("Running without AOT");
Reference a project from a file‑based app
You can reference projects via the #:project directive—no .csproj required for the app.
// app.cs
#:project ..\ClassLib\ClassLib.csproj
var greeter = new ClassLib.Greeter();
Console.WriteLine(greeter.Greet(args.Length > 0 ? args[0] : "World"));
dotnet run .\app.cs Roxeem
# Output:
# Hello, Roxeem!
Shebang (Unix/macOS)
On Unix shells, you can run file‑based apps directly with a shebang. Example shown for completeness if you’re cross‑platform:
#!/usr/bin/env dotnet
Console.WriteLine("Hello shebang!");
Then mark executable and run (chmod +x app), no dotnet run needed. On Windows, prefer dotnet run <file>.cs.
Bonus for tool authors: platform‑specific tools and the any RID
If you author .NET tools, .NET 10 lets you ship multiple RIDs in one package, and even include any for broad compatibility. Example csproj snippet:
<PropertyGroup>
<PackAsTool>true</PackAsTool>
<RuntimeIdentifiers>
linux-x64;linux-arm64;macos-arm64;win-x64;win-arm64;any
</RuntimeIdentifiers>
</PropertyGroup>
You can also use the full suite of publish options (self‑contained, trimmed, AOT) for tools, not just apps.
Practical CI examples
Ad‑hoc formatting in a pipeline:
dotnet tool exec dotnet-format --verify-no-changes
Deterministic restore of repo‑pinned tools:
dotnet tool restore
dotnet dotnetsay --version
Quick local run with dnx:
dnx dotnetsay "CI passed!"
Cheat sheet
- Ad‑hoc tools:
dotnet tool exec <pkg>[@ver] [args](use--non-interactivein CI;--sourcefor feeds) - dnx runner:
dnx <tool> [args](super short; great for local use) - File‑based apps:
dotnet run <file>.csanddotnet publish <file>.cs -r <rid> - Disable AOT for file‑based apps:
#:property PublishAot=false - Reference a project from a file:
#:project <path-to-.csproj>

