Writing Utilities with .NET Core Tools
I don't know about you but if you're like me, you have 50 trillion little snippets of code and half-developed apps all over the place on your dev machine. It's all those little one-off applications you wrote to do some kind of little thing that you maybe need to use once or twice a year. Well, did you know that you can actually package them up, publish them, and share them on NuGet? Yah. Rather than letting all that code just sit around doing nothing, you can share them as a .NET Core Tool.
BTW, I've got a couple of free and open-source .NET Core tools that you might be interested in. Solution Utils (slnutil) is a collection of utilities for working with .NET Core projects and solutions. The other one is Azure DevOps Utils (azdoutil) and that has a bunch of utilities that I've written for working with Azure DevOps. And if you're feeling wildly inspired to write your own .NET Core tools, you might find my Commands Framework NuGet package helpful.
Also, if you want the sample code for this article, it's published to https://github.com/benday-inc/demo-dotnet-tool.
What is a .NET Core Tool?
A .NET Core tool is basically a command-line app that you can install and run using the dotnet CLI. It's kind of like a plugin or utility that helps you automate tasks or add features to your development workflow. For example, you might install a tool that helps you generate code, lint your projects, or manage database migrations — and then run it with a simple command like dotnet mytool or even just mytool.
There are two kinds of .NET Core tools: global tools and local tools. Global tools are installed system-wide and can be run from anywhere in your terminal, while local tools are tied to a specific project and defined in a config file so teammates can easily use the same version. It's a nifty little way to package and share .NET-based utilities without needing a full-blown installer or script.
How do you write a .NET Core Tool?
A .NET Core tool is basically just a glorified console application. So to get started you either create a console app from Visual Studio or run dotnet new console from the command line. After that, you'll need to make a handful of tweaks to your project file:
- Turn on Pack as Tool
- Turn on GeneratePackageOnBuild
- (Recommended) Change your Assembly Name to be something short and command-line-friendly
- (Optional) Give your application a Title
- (Optional) Set your Author name to be your NuGet publisher name
You can make these changes using VSCode or a text editor but I feel like Visual Studio 2022 make these changes super easy. To make these changes, open your Solution in Visual Studio 2022, right-click on your console application, and choose Properties from the context menu.
That'll bring up the project properties editor. In the left-side menu, there's a section named Package. Expand the Package section and then choose General.
[Click on image for larger view.] The Package | General section of the Visual Studio Project Editor
You should now see a section that looking like the image below.
[Click on image for larger view.] Check the Generate NuGet package on build checkbox. Give your app a Title.
If you open up your console project's csproj file, it should look something like this. Notice the Title and GeneratePackageOnBuild values are now set.
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Benday.HelloWorldUtil.Api\Benday.HelloWorldUtil.Api.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>Hello World .NET Core Tool</Title>
</PropertyGroup>
</Project>
Back in the Visual Studio Project Properties editor, as you scroll down on the Package section, you'll see that there a ton more options that you can set. The next important one that you need to adjust is Pack as .NET tool. Make sure that that Pack as .NET tool checkbox is checked. That'll add a <PackAsTool>True</PackAsTool>
; property to your csproj file.
[Click on image for larger view.]
The last change that I'd tell you to make is to the assembly name. If you're going to use this as a command line tool, you're not going to want to type some super-long command. You'll want something short and pithy. In the Properties editor, go to the Application section and choose General. Scroll down a little and then you'll see a textbox for Assembly name. This controls what the generated file is when you build the app. In my case, I'm setting it to helloworldutil
.
[Click on image for larger view.]
And that's it for project file changes.
So what does the code look like?
So far, this has been a whole lot about configuring the project. What does the actual .NET Tool code look like? Well, the surprisingly awesome thing is that the code is just like any other piece of console app code.
Here's what I wrote in the sample app's Program.cs
file. All that stuff about IDotnetToolCommand is my own code. Overall, there's nothing special about this Program.cs
.
if (args.Length == 0)
{
DisplayUsage();
return;
}
else
{
var commandName = args[0].ToLowerInvariant();
IDotnetToolCommand command;
if (commandName == "hello")
{
command = new HelloWorldCommand(args);
}
else if (commandName == "random")
{
command = new RandomNumberCommand(args);
}
else if (commandName == "now")
{
command = new CurrentDateTimeCommand(args);
}
else
{
Console.WriteLine($"Unknown command: {commandName}");
DisplayUsage();
return;
}
}
A little bit about the demo app
When I write .NET Core Tool applications, I usually try to organize each of the pieces of functionality into a class that I call a "command". In the demo app for this article, I've got 3 commands: CurrentDateTimeCommand, HelloWorldCommand, and RandomNumberCommand. None of these are particularly complex. The commands display the current date & time, say "hello world", or display a random number between 1 and 100.
[Click on image for larger view.]
Installing the .NET Tool locally
Whether you're planning to publish your tool to NuGet or not, you're going to need a way to install it locally so you can test it out during development. I've included a handy little powershell script that take the generated NuGet package and installs it locally without having to go publish anything to NuGet.org.
[CmdletBinding()]
param([Parameter(HelpMessage='Uninstall before installing')]
[ValidateNotNullOrEmpty()]
[switch]
$reinstall)
if ($reinstall -eq $true)
{
&.\uninstall.ps1
}
dotnet build
$pathToDebugFolder = Join-Path $PSScriptRoot 'src\Benday.HelloWorldUtil.ConsoleUi\bin\Debug'
Write-Host "Installing Benday.HelloWorldUtil.ConsoleUi from $pathToDebugFolder"
dotnet tool install --global --add-source "$pathToDebugFolder" helloworldutil
It's almost all about that last line that starts with dotnet tool install --global. That installs it as a global tool meaning that you can run it anywhere on your machine from a Terminal window.
Running the .NET Tool
Once you've installed it, just pop open a Terminal window and run the command. The sample app is helloworldutil and when you run it without any command args, you'll see a usage screen similar to the image below.
[Click on image for larger view.]
From there, you can run any of the individual commands:
- helloworldutil hello
- helloworldutil random
- helloworldutil now
Summary
In this article, I walked through how to take a simple console app and turn it into a reusable .NET Core tool. I explained what .NET Core tools are, how they work, and the difference between global and local tools. I showed how to configure your project to build and package it correctly, how to write the command-handling code, and how to install and test your tool locally using the dotnet CLI. If you've got small utilities sitting around your dev machine, hopefully this gave you a practical path to clean them up and make them shareable.
Looking for the sample code? You can find the sample code here.
About the Author
Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.
Posted by Benjamin Day on 06/18/2025