LinhGo Labs
LinhGo Labs
Build Your Own CLI Tool with .NET 8: A Complete Guide

Build Your Own CLI Tool with .NET 8: A Complete Guide

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.


Before we start, make sure you have:

What You NeedWhy
.NET 8 SDKObviously - we’re building a .NET tool
OpenWeatherMap API KeyFree sign up here - takes 2 minutes
TerminalAny will work - Bash, PowerShell, CMD, doesn’t matter
Optional: VS CodeMakes life easier, but not required

Got everything? Great, let’s build something.


First things first - let’s create a new .NET console application:

dotnet new console -n WeatherCliTool
cd WeatherCliTool

This creates a new folder called WeatherCliTool with a basic “Hello World” app. Nothing fancy yet, but it’s a start.


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:

PropertyWhat It Does
PackAsToolTells .NET “hey, this should be a CLI tool, not just a regular app”
ToolCommandNameThe actual command users will type (we chose weathercli, but could be anything)
TargetFrameworkWe’re using .NET 8 (latest and greatest)

This configuration is what transforms a boring console app into a proper global CLI tool.


We need to make HTTP calls and parse JSON responses. One package does both:

dotnet add package System.Net.Http.Json

This package is from Microsoft and handles all the heavy lifting for API calls and JSON parsing.


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:

  1. Arguments check - If someone runs weathercli without a city name, show them how to use it
  2. API key - Grab it from environment variables (more on this next)
  3. HTTP call - Fetch weather data from OpenWeatherMap
  4. Parse & display - Convert JSON to C# objects and show nice formatted output
  5. 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!


Never, ever hardcode API keys in your source code. Instead, use environment variables:

export OPENWEATHER_API_KEY=your_api_key_here

Add this to your ~/.bashrc or ~/.zshrc to make it permanent.

setx OPENWEATHER_API_KEY your_api_key_here
$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.


Time to turn this into a distributable package:

dotnet pack -c Release

This creates a .nupkg file (basically a zip with your tool inside) at:

bin/Release/WeatherCliTool.1.0.0.nupkg

Think of this as your tool’s installation package - like a .deb file for Ubuntu or an .msi for Windows.


Now let’s install it system-wide so you can use it from anywhere:

dotnet tool install --global --add-source ./bin/Release WeatherCliTool

What this does:

  • --global makes it available everywhere (not just in this folder)
  • --add-source ./bin/Release tells .NET where to find your .nupkg file

Once installed, the tool is added to your PATH automatically. You can now use it from any directory.


Moment of truth - let’s test it:

weathercli Hanoi
weathercli "New York"
weathercli Tokyo

If everything worked, you should see something like:

๐ŸŒ City: Hanoi
๐ŸŒก Temperature: 32.5ยฐC
๐ŸŒฆ Condition: Clear (clear sky)
๐Ÿ’จ Wind: 2.3 m/s

Pretty cool, right? You just built a real CLI tool that anyone can install and use.


dotnet tool uninstall --global weathercli

This is a basic version, but you could add some really cool features:

EnhancementWhat It Adds
--help flagUsage instructions (always nice to have)
System.CommandLineProfessional argument parsing (colors, validation, etc.)
Spectre.ConsoleBeautiful terminal output with tables and progress bars
CachingSave recent queries - avoid hitting API limits
Multiple unitsAdd --fahrenheit flag for American users
JSON outputAdd --json for piping to other tools
ForecastShow 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.



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:

StepWhat We Did
1Created a console app
2Configured it as a CLI tool in .csproj
3Added HTTP/JSON handling
4Wrote the weather fetching logic
5Set up API key securely
6Packaged it with dotnet pack
7Installed it globally
8Tested 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!