DEV Community

Laurent Mondeil for Onepoint

Posted on

Concevoir une CLI moderne en .NET : retour d’expérience

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>>
Enter fullscreen mode Exit fullscreen mode

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;}

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Résultat : une aide en ligne claire, générée automatiquement, et sans effort supplémentaire à maintenir.

TheSuperCli -h
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode

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>>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

decrit l'utilisation de l'appsettings pour le generic host

(1) Lecture des valeurs de connection-string et database

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>>
Enter fullscreen mode Exit fullscreen mode

décris comment persister des paramètre de commande en appsettings

TheSuperCli settings show
Enter fullscreen mode Exit fullscreen mode

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.

1 appsettings par environnement

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
Enter fullscreen mode Exit fullscreen mode

Selon la présence ou non du paramètre --env, la CLI :

  • met à jour appsettings.json (--env absent),
  • ou cible un fichier app.settings.<<env>>.json spécifique à l’environnement (--env pré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>>
Enter fullscreen mode Exit fullscreen mode

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

schéma descriptif de la persistence d'un paramètre dans l'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

décrit la copie de l'appsettings spécifique à l'environnement en appsettings par défaut

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>>.json en appsettings.json

décrit la suppression d'un appsettings en vue de son remplacement

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>>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)