Dans de nombreux projets, les opérations de maintenance finissent par devenir un sujet à part entière. C’est exactement ce qui nous est arrivé lors d’un projet reposant sur Azure Cosmos DB.
Très vite, nous avons dû mettre en place des opérations récurrentes : suppression de documents selon certains critères, sauvegardes ponctuelles, vérifications ciblées… Autant de tâches qu’il fallait pouvoir exécuter aussi bien depuis nos postes de travail que depuis des serveurs headless.
Dans cet article, je vous propose un retour d’expérience sur la conception d’une CLI en .NET, pensée pour être :
- structurée,
- évolutive,
- simple à distribuer,
- et confortable à utiliser au quotidien.
Du script à l’outil… puis à la complexité
Au départ, la solution semblait évidente : créer une petite application console capable d’exécuter les opérations nécessaires sur la base Cosmos DB.
La première version répondait au besoin de suppression. Une seconde a rapidement vu le jour pour gérer les sauvegardes. Puis une troisième est venue couvrir un besoin plus spécifique.
Le problème ?
Chaque nouvel outil partageait une large base de code commune. La multiplication de ces applications devenait contre‑productive et difficile à maintenir.
Nous avons donc décidé de fusionner l’ensemble dans un seul outil. Techniquement, l’objectif était atteint… mais un nouveau problème est apparu : la lisibilité de l’interface en ligne de commande.
TheSuperApp -operation save -saveoption file -format json -location "c:\everything\backup-01.json" \
-query "SELECT * FROM c WHERE c.objectType = 'obsolete'" \
-connectionstring <<connectionstring>> -database <<database>>
TheSuperApp -operation delete \
-query "SELECT * FROM c WHERE c.objectType = 'obsolete'" \
-connectionstring <<connectionstring>> -database <<database>>
Avec ce type d’appel, il devenait rapidement difficile de savoir :
- quels paramètres étaient obligatoires,
- lesquels s’appliquaient à une opération donnée,
- et comment faire évoluer l’outil sans le rendre encore plus illisible.
Il devenait clair qu’il fallait repenser l’interface de la CLI, pas seulement son implémentation.
Structurer la CLI autour de verbes
La première décision structurante a été de concevoir la CLI autour de verbes représentant des actions, à la manière des commandes Git ou Docker.
Plutôt qu’un bloc unique de paramètres, nous avons opté pour :
- des commandes de premier niveau (
save,select,delete), - puis des sous-commandes pour préciser le comportement (
to-file,to-litedb, etc.), - chacune disposant de ses propres paramètres.
Cette approche permet :
- une meilleure lisibilité,
- une aide en ligne plus claire,
- et une évolutivité bien plus saine.
---
config:
look: neo
theme: forest
layout: elk
wrap: true
---
flowchart LR
classDef cli fill:lightgreen
classDef parameter fill:#bcbccc
classDef globalparameter fill:#feaf5f
classDef value fill:#75757f,color:white
cli:::cli --> save & select & delete
save --> to-file & to-litedb
to-file --> query1>query]:::parameter & location>location]:::parameter & format>format]
format:::parameter --> json((json)):::value & yaml((yaml)):::value
to-litedb --> query2>query]:::parameter & litedbconnectionstring>litedbconnection-string]:::parameter
select --> query3>query]:::parameter
delete --> query4>query]:::parameter
select & delete & to-file & to-litedb ----> cnx>connection-string]:::parameter
select & delete & to-file & to-litedb ----> db>database]:::parameter
Implémentation avec CommandLineUtils
Afin de ne pas repartir de zéro, nous avons choisi d’utiliser CommandLineUtils de McMaster. La bibliothèque coche rapidement toutes les cases :
- hiérarchisation naturelle des commandes et sous‑commandes,
- génération automatique de l’aide,
- intégration avec le Generic Host de .NET.
La définition de la hiérarchie et des paramètres se fait directement via des attributs :
- Attributs de classe pour a hiérarchie (Command, Subcommand)
- Attributs de propriété pour les paramètres (Option, Argument)
[Command("save")]
[Subcommand(typeof(ToFileCommand), typeof(ToLiteDbCommand))]
class SaveCommand
{
// ...
}
[Command("to-file")]
class ToFileCommand
{
[Option]
public string Location {get; set;}
// ...
}
Résultat : une aide en ligne claire, générée automatiquement, et sans effort supplémentaire à maintenir.
TheSuperCli -h
Usage: TheSuperCli [command] [options]
Options:
-f|--full-help FullHelp
-?|-h|--help Show help information.
Commands:
delete
select
save
Run 'TheSuperCli [command] -?|-h|--help' for more information about a command.
Distribuer l’outil simplement : le choix du .NET Tool
Une fois l’outil fonctionnel, s’est posée la question de sa diffusion.
Nous avons opté pour .NET Tool qui est un format de package nuget particulier.
Ce choix apporte des bénéfices immédiats :
- La distribution s'effectue depuis n'importe quel feed nuget - nous avons choisi nuget.org,
- L'installation ne nécessite pas de privilège administrateur,
- L'installation et la mise à jour s'effectuent en ligne de commande (pratique pour les systèmes headless),
- L'obtention d'une version spécifique est un argument de la ligne de commande,
- Il n'y a pas d'autre prérequis que .NET Tool.
dotnet tool install <<TheSuperApp>>
dotnet tool update <<TheSuperApp>>
Le packaging est simple : quelques propriétés suffisent dans le fichier projet pour transformer une application console en outil installable globalement.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
[...]
<PackAsTool>true</PackAsTool>
<ToolCommandName>contacts</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
</PropertyGroup>
</Project>
Gérer le contexte
À l’usage, un point d’irritation est vite apparu : devoir ressaisir à chaque exécution la chaîne de connexion et le nom de la base de données.
Grâce à l’utilisation du Generic Host, nous avons pu exploiter les fichiers de configuration tels que appsettings.json.
Dans une CLI (sans IOptions), ces fichiers sont lus au démarrage et peuvent être modifiés par l’application elle‑même par la suite.
(1) Lecture des valeurs de
connection-stringetdatabase
Nous avons donc ajouté des commandes permettant :
- d’enregistrer la chaîne de connexion,
- de stocker le nom de la base de données,
- et de les afficher à tout moment.
TheSuperCli settings set --connection-string <<connectionstring>>
TheSuperCli settings set --database <<database>>
TheSuperCli settings show
Nous n'avions alors plus à saisir ces paramètres en ligne de commande une fois enregistrés.
Un pas de plus : la gestion multi‑environnements
Un dernier point est apparu assez naturellement : un seul jeu de paramètres ne suffisait plus.
Nous travaillions sur trois environnements distincts (dev, rec, prd).
Nous avons donc introduit la notion d’environnement, chacun d'entre eux correspondant à un fichier appsettings.<env>.json.
Les commandes inhérentes aux settings ont ainsi été enrichies de sorte à accueillir le paramètre --env
exemple
TheSuperCli settings set --database <<database>> --env prod
Selon la présence ou non du paramètre --env, la CLI :
- met à jour
appsettings.json(--envabsent), - ou cible un fichier
app.settings.<<env>>.jsonspécifique à l’environnement (--envprésent).
Des commandes supplémentaires permettent :
- de sauvegarder un environnement,
- de basculer de l’un à l’autre,
- ou de supprimer une configuration devenue inutile.
Plus de détails au paragraphe Gestion multi-environnements
---
config:
look: neo
theme: forest
layout: elk
wrap: true
---
flowchart LR
classDef cli fill:lightgreen
classDef parameter fill:#bcbccc
classDef globalparameter fill:#feaf5f
classDef value fill:#75757f,color:white
envparam>env]:::parameter
TheSuperCli:::cli --> settings --> set --> connection-string & database
connection-string --> cnxstringparam>connection-string]:::parameter
connection-string ---> envparam
database --> databaseparam>database]:::parameter
database ---> envparam
TheSuperCli --> show ---> envparam
TheSuperCli --> save ---> envparam
TheSuperCli --> switchto ---> envparam
TheSuperCli --> delete ---> envparam
Démarrer plus vite grâce à un template
Pour capitaliser sur ce travail, un template de CLI .NET disponible sur nuget.org intègre
CommandLineUtils,
le packaging en .NET Tool,
et la gestion des environnements.
dotnet new install lmondeil.cli.template
dotnet new lmondeil.cli --name <<cli name>>
En quelques minutes, vous disposez d’un socle propre, structuré et prêt à être spécialisé.
Conclusion
Ce retour d’expérience montre qu’une CLI est bien plus qu’un simple point d’entrée technique.
Lorsqu’elle est pensée comme un produit à part entière, elle apporte :
- de la clarté,
- de la robustesse,
- et un vrai confort d’usage au quotidien.
Annexes
Gestion multi-environnements
Le fonctionnement des commandes de gestion de environnement est le suivant
settings set
TheSuperCli settings set --connection-string <<connectionstring>> --env <<env>>
TheSuperCli settings set --database <<database>> --env <<env>>
Définit les paramètres de l'environnement actuel (non nommé)
⇒ enregistre le paramètre spécifié dans le fichier appsettings.<>.json
settings save
TheSuperCli settings save --env <<env>>
Enregistre les paramètres actuels en tant que paramètres d'un environnement spécifique
⇒ copie app.settings.json en app.settings.<<env>>.json
settings switchto
Charge les paramètres de l'environnement <<env>>
⇒ remplace le contenu deappsettings.json par celui de app.settings.<<env>>.json
- Suppression de appsettings.json
- copie de
appsettings.<<env>>.jsonenappsettings.json
Publier un package nuget sur nuget.org
Cette opération nécessite que vous ayez obtenu une clé d'api auprès de nuget.org
dotnet nuget push <<PathToPackage>>.nupkg --source https://api.nuget.org/v3/index.json --api-key <<NuGet API key>>






Top comments (0)