api-versioning
API versioning strategies for ASP.NET Core. Covers Asp.Versioning library, URL segment, header, and query string strategies,…
Console apps and CLI tools on .NET 10: System.CommandLine 2.x (RootCommand, Option<T>, SetAction, InvokeAsync), Generic Host + DI in console apps, exit codes, stdout/stderr discipline, Ctrl+C cancellation, interactive vs piped use, dotnet tool packaging, and `dotnet run app.cs`
Claude Code
$ npx -y skills add codewithmukesh/dotnet-claude-kit --skill console-cli --agent claude-code
Codex
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a codex -y
Cursor
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a cursor -y
Gemini CLI
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a gemini-cli -y
GitHub Copilot
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a github-copilot -y
OpenCode
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a opencode -y
Windsurf
$ npx skills add codewithmukesh/dotnet-claude-kit --skill console-cli -a windsurf -y
name: console-cli description: > Console apps and CLI tools on .NET 10: System.CommandLine 2.x (RootCommand, Option<T>, SetAction, InvokeAsync), Generic Host + DI in console apps, exit codes, stdout/stderr discipline, Ctrl+C cancellation, interactive vs piped use, dotnet tool packaging, and `dotnet run app.cs` file-based apps. Load when the user mentions "console app", "CLI", "command line", "System.CommandLine", "dotnet tool", "exit code", "stdin", "args parsing", or "file-based app".
1. **stdout is data, stderr is everything else** — Results go to stdout so `mytool list | jq` works. Logs, progress, prompts and errors go to stderr. Mixing them breaks every pipe and script that consumes your tool. 2. **Exit codes are the API** — `0` = success, non-zero = failure, and each failure class gets a stable code. Scripts and CI branch on exit codes, never on parsing your error text. 3. **Use System.CommandLine 2.x, not hand-rolled `args` parsing** — It went stable at 2.0 and gives help, `--version`, validation, tab completion and Ctrl+C handling for free. Hand parsing always misses `--opt=value`, quoting, and help output. 4. **Cancellation flows from Ctrl+C to every await** — System.CommandLine turns SIGINT/SIGTERM into a `CancellationToken`. Pass it all the way down, or Ctrl+C becomes a hard kill that corrupts half-written files. 5. **Generic Host only when you need it** — DI, configuration, logging and `IHttpClientFactory` justify `Host.CreateApplicationBuilder`. A 50-line utility does not; startup time matters for CLIs.
Verified against System.CommandLine 2.0.x (stable). The beta-era APIs (`SetHandler`, `InvocationContext`, `CommandLineBuilder`) are gone.
using System.CommandLine;
Option<FileInfo> inputOption = new("--input", "-i")
{
Description = "File to import",
Required = true
};
inputOption.AcceptExistingOnly();
Option<int> batchOption = new("--batch-size")
{
Description = "Rows per batch",
DefaultValueFactory = _ => 500
};
batchOption.Validators.Add(result =>
{
if (result.GetValue(batchOption) is < 1 or > 10_000)
result.AddError("--batch-size must be between 1 and 10000");
});
Option<bool> dryRunOption = new("--dry-run") { Description = "Validate without writing" };
Command importCommand = new("import", "Import orders from a CSV file")
{
inputOption, batchOption, dryRunOption
};
importCommand.SetAction((parseResult, ct) => ImportCommand.RunAsync(
parseResult.GetValue(inputOption)!,
parseResult.GetValue(batchOption),
parseResult.GetValue(dryRunOption),
ct));
RootCommand root = new("Order tooling") { importCommand };
return await root.Parse(args).InvokeAsync();**Why:** options are typed objects, so `GetValue` is compile-time checked. `InvokeAsync` prints parse errors to stderr, shows help, and returns exit code `1` without your code running. `Required`, `AcceptExistingOnly()`, and `Validators` keep input validation at the boundary.
public static class ExitCodes
{
public const int Success = 0;
public const int Failure = 1; // also what System.CommandLine returns on parse errors
public const int ValidationFailed = 2;
public const int NotFound = 3;
public const int Cancelled = 130; // 128 + SIGINT, the shell convention
}
public static class ImportCommand
{
public static async Task<int> RunAsync(FileInfo input, int batchSize, bool dryRun, CancellationToken ct)
{
try
{
var rows = await CsvReader.ReadAsync(input, ct);
if (rows.Errors is [_, ..])
{
foreach (var error in rows.Errors)
await Console.Error.WriteLineAsync(error);
return ExitCodes.ValidationFailed;
}
if (!dryRun)
await OrderImporter.ImportAsync(rows.Items, batchSize, ct);
Console.WriteLine(rows.Items.Count); // the data result → stdout
return ExitCodes.Success;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
await Console.Error.WriteLineAsync("Cancelled.");
return ExitCodes.Cancelled;
}
}
}**Why:** named constants document the contract and stop magic numbers drifting. Catching `OperationCanceledException` only when the token fired distinguishes a user Ctrl+C from a real timeout bug. Document the codes in `--help` text or the README.
`ParseResult.InvokeAsync` cancels the action's token on Ctrl+C, SIGINT and SIGTERM. `InvocationConfiguration.ProcessTerminationTimeout` (default 2 seconds) is how long the action gets to finish before the process is killed.
InvocationConfiguration config = new()
{
ProcessTerminationTimeout = TimeSpan.FromSeconds(10) // time to flush a partial batch
};
return await root.Parse(args).InvokeAsync(config);**Why:** the default 2 seconds suits most tools; raise it only when cleanup (flushing, releasing a lock file) genuinely takes longer. Never install your own `Console.CancelKeyPress` handler alongside it — two owners of Ctrl+C race each other.
There is no stable `System.CommandLine.Hosting` package (it is still alpha), so compose them yourself: parse first, then build the host inside the action that needs services.
Option<bool> verboseOption = new("--verbose", "-v") { Recursive = true }; // added to root, inherited by subcommands
importCommand.SetAction(async (parseResult, ct) =>
{
using var host = AppHost.Build(parseResult.GetValue(verboseOption));
var handler = host.Services.GetRequiredService<ImportHandler>();
return await handler.RunAsync(parseResult.GetValue(inputOption)!, ct);
});
internal static class AppHost
{
public static IHost Build(boolAPI versioning strategies for ASP.NET Core. Covers Asp.Versioning library, URL segment, header, and query string strategies,…
Architecture conformance check: verifies an existing codebase against its declared architecture (VSA, Clean Architecture, DDD,…
Architecture selection advisor for .NET applications. Asks structured questions about domain complexity, team size, system…
.NET Aspire for cloud-native orchestration. Covers AppHost configuration, service defaults, resource configuration, service…
Authentication and authorization for ASP.NET Core. Covers JWT bearer tokens, OpenID Connect, ASP.NET Identity, authorization…
Avalonia UI 12 for cross-platform .NET desktop apps: compiled bindings (x:DataType, on by default in 12),…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.