How to build a professional command-line tool with .NET 8 - from basic setup to packaging and distribution.
Ever wanted to build your own CLI tool like npm, docker, or git? Something you can run from anywhere with just a command? It’s actually easier than you might think.
In this guide, we’ll build weathercli - a command-line tool that fetches current weather for any city using the OpenWeatherMap API. By the end, you’ll know how to:
- Create a proper .NET CLI application
- Package it as a global tool
- Install it system-wide
- Distribute it to others
Let’s dive in.
Prerequisites
Before we start, make sure you have:
| What You Need | Why |
|---|---|
| .NET 8 SDK | Obviously - we’re building a .NET tool |
| OpenWeatherMap API Key | Free sign up here - takes 2 minutes |
| Terminal | Any will work - Bash, PowerShell, CMD, doesn’t matter |
| Optional: VS Code | Makes life easier, but not required |
Got everything? Great, let’s build something.
Create the Console App
First things first - let’s create a new .NET console application:
dotnet new console -n WeatherCliTool
cd WeatherCliToolThis creates a new folder called WeatherCliTool with a basic “Hello World” app. Nothing fancy yet, but it’s a start.
Configure the Project for CLI Tools
Here’s where it gets interesting. Open WeatherCliTool.csproj and replace everything with:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>weathercli</ToolCommandName>
</PropertyGroup>
</Project>The magic happens in these two lines:
| Property | What It Does |
|---|---|
PackAsTool | Tells .NET “hey, this should be a CLI tool, not just a regular app” |
ToolCommandName | The actual command users will type (we chose weathercli, but could be anything) |
TargetFramework | We’re using .NET 8 (latest and greatest) |
This configuration is what transforms a boring console app into a proper global CLI tool.
Add Required Packages
We need to make HTTP calls and parse JSON responses. One package does both:
dotnet add package System.Net.Http.JsonThis package is from Microsoft and handles all the heavy lifting for API calls and JSON parsing.
Write the Weather CLI Logic
Now for the fun part - let’s write the actual code. Replace everything in Program.cs with:
using System.Net.Http;
using System.Net.Http.Json;
class Program
{
static async Task Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Usage: weathercli <city>");
Console.WriteLine("Example: weathercli Tokyo");
return;
}
string city = string.Join(" ", args);
string? apiKey = Environment.GetEnvironmentVariable("OPENWEATHER_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("โ ERROR: OpenWeatherMap API key not found.");
Console.WriteLine("Set it using: export OPENWEATHER_API_KEY=your_key");
return;
}
string url = $"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={apiKey}&units=metric";
try
{
using HttpClient client = new();
var weather = await client.GetFromJsonAsync<WeatherResponse>(url);
if (weather is not null)
{
Console.WriteLine($"\n๐ City: {weather.Name}");
Console.WriteLine($"๐ก Temperature: {weather.Main.Temp}ยฐC");
Console.WriteLine($"๐ฆ Condition: {weather.Weather[0].Main} ({weather.Weather[0].Description})");
Console.WriteLine($"๐จ Wind: {weather.Wind.Speed} m/s\n");
}
else
{
Console.WriteLine("โ Could not retrieve weather data.");
}
}
catch (HttpRequestException)
{
Console.WriteLine("โ Network error. Please check your internet connection.");
}
catch (Exception ex)
{
Console.WriteLine($"โ Unexpected error: {ex.Message}");
}
}
}
// API Response Models
public class WeatherResponse
{
public string Name { get; set; }
public MainWeather Main { get; set; }
public List<WeatherInfo> Weather { get; set; }
public Wind Wind { get; set; }
}
public class MainWeather
{
public double Temp { get; set; }
}
public class WeatherInfo
{
public string Main { get; set; }
public string Description { get; set; }
}
public class Wind
{
public double Speed { get; set; }
}What’s happening here:
- Arguments check - If someone runs
weathercliwithout a city name, show them how to use it - API key - Grab it from environment variables (more on this next)
- HTTP call - Fetch weather data from OpenWeatherMap
- Parse & display - Convert JSON to C# objects and show nice formatted output
- Error handling - Catch network issues and other problems gracefully
The models at the bottom (WeatherResponse, MainWeather, etc.) map to the JSON structure that OpenWeatherMap returns. .NET does the JSON parsing automatically - pretty neat!
Set Up Your API Key (Securely!)
Never, ever hardcode API keys in your source code. Instead, use environment variables:
Linux / macOS:
export OPENWEATHER_API_KEY=your_api_key_hereAdd this to your ~/.bashrc or ~/.zshrc to make it permanent.
Windows (CMD):
setx OPENWEATHER_API_KEY your_api_key_hereWindows (PowerShell):
$env:OPENWEATHER_API_KEY = "your_api_key_here"For PowerShell, add this to your profile ($PROFILE) to make it stick.
Pro tip: Restart your terminal after setting the variable so it picks up the change.
Package Your Tool
Time to turn this into a distributable package:
dotnet pack -c ReleaseThis creates a .nupkg file (basically a zip with your tool inside) at:
bin/Release/WeatherCliTool.1.0.0.nupkgThink of this as your tool’s installation package - like a .deb file for Ubuntu or an .msi for Windows.
Install the CLI Globally
Now let’s install it system-wide so you can use it from anywhere:
dotnet tool install --global --add-source ./bin/Release WeatherCliToolWhat this does:
--globalmakes it available everywhere (not just in this folder)--add-source ./bin/Releasetells .NET where to find your.nupkgfile
Once installed, the tool is added to your PATH automatically. You can now use it from any directory.
Use Your Shiny New CLI Tool!
Moment of truth - let’s test it:
weathercli Hanoi
weathercli "New York"
weathercli TokyoIf everything worked, you should see something like:
๐ City: Hanoi
๐ก Temperature: 32.5ยฐC
๐ฆ Condition: Clear (clear sky)
๐จ Wind: 2.3 m/sPretty cool, right? You just built a real CLI tool that anyone can install and use.
Uninstall the Tool
dotnet tool uninstall --global weathercliTaking It Further
This is a basic version, but you could add some really cool features:
| Enhancement | What It Adds |
|---|---|
--help flag | Usage instructions (always nice to have) |
System.CommandLine | Professional argument parsing (colors, validation, etc.) |
Spectre.Console | Beautiful terminal output with tables and progress bars |
| Caching | Save recent queries - avoid hitting API limits |
| Multiple units | Add --fahrenheit flag for American users |
| JSON output | Add --json for piping to other tools |
| Forecast | Show 5-day forecast instead of just current weather |
The System.CommandLine and Spectre.Console packages are particularly worth looking into - they make CLI tools feel really polished.
Resources
- OpenWeatherMap API Docs - All the weather endpoints
- System.Net.Http.Json Package - HTTP + JSON made easy
- .NET Global Tools - Official Microsoft docs
Wrapping Up
Congrats! You just built a real, working CLI tool with .NET 8. Not bad for a few hours of work, right?
Here’s what we covered:
| Step | What We Did |
|---|---|
| 1 | Created a console app |
| 2 | Configured it as a CLI tool in .csproj |
| 3 | Added HTTP/JSON handling |
| 4 | Wrote the weather fetching logic |
| 5 | Set up API key securely |
| 6 | Packaged it with dotnet pack |
| 7 | Installed it globally |
| 8 | Tested it out! |
The cool thing about .NET global tools is you can publish them to NuGet and anyone in the world can install your tool with just dotnet tool install -g YourToolName. That’s how tools like dotnet-ef and dotnet-aspnet-codegenerator work.
What will you build next? A task manager? A file converter? A deployment tool? The possibilities are endless. Drop a comment if you build something cool!
Happy coding!





