System.CommandLine Helpful Patterns #2505
JaimeStill
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
System.CommandLine Helpful Patterns
This document, and the corresponding repository, illustrate patterns that I've found to be useful when working with the
System.CommandLinelibrary. It is my hope that they can be of value to anyone who stumbles across it. If you have any feedback or additional patterns to share, please do!The CLI app built out through this demonstration perfoms simple symmetric encryption / decryption of a specified file using a specified
Guidas the private key.Each section illustrates the implementation of a specific concept, and each subsequent section enhances the capability or development experience of the overall app structure.
The topics introduced are:
App and Commands - Create infrastructure that simplifies the initialization of a CLI app and the creation of commands / sub-commands. This section also scaffolds the initial project that the rest of the sections will build on.
Runners - Bind the initial state of the command from provided options to the properties of a class that corresponds to a standard delegate definition. This greatly simplifies the process of defining the delegate executed by a command.
IConfigurationinstance.Note
The specific capability demonstrated is trivial and purely intended to illustrate patterns for working with
System.CommandLine.App and Commands
This section will form the basis for the rest of the patterns that will be shown. It provides what, in my experience, has been an excellent starting point for building a robust CLI app with numerous sub-commands just by standardizing and simplifying the process of initializing the app and creating the commands.
Initial setup:
Create the
SclPatterns.slnfile:Initialize the console app:
Add initial dependencies:
Tip
Code snippet headers will indicate the full file path of the file relative to the project root.
If the project is hosted at
/home/SclPatterns/srcand a file is located at/home/SclPatterns/src/Cli/ICliCommand.cs, the header will beCli/ICliCommand.cs.Core Infrastructure
The files that follow provide the basic primitives for standardizing and simplifying the CLI app.
Cli/ICliCommand.csAll
CliCommandinstances will define aBuildfunction that returns aCommand.Cli/CliCommand.csA
CliCommandis initialized with all of the state needed to generate aCommandthrough theBuildmethod.optionsdefines all of the options purely needed for this command.globalsdefines all of the options that should be available from this command down to its deepest sub-commands.Sub-commands are added to the returned
Commandby selecting the result of their ownBuildfunction.Cli/CliApp.csThe
CliAppclass adds all of its direct commands by selecting the result of theirBuildmethod and adding it to theRootCommand.globalsis used to define any options that shuold be globally available from this level down to the deepest sub-command.CliDefaults.csThe
CliDefaultsstatic class provides a helpful point of reference fo default values that should not change.Example Setup
The files that follow define the starting functionality for the CLI app, which facilitates local file encryption / decryption with a global
Guid keyoption.Models/EncryptedFile.csThis class serves as the model for storing metadata and encrypted file data. It also provides methods needed to serialize / deserialize the model instance to and from JSON on the local file system.
Commands/EncryptCommand.csThis command takes a
FileInfo source(required) and aDirectoryInfo target(default:CliDefaults.AppPath), as well as the globalGuid keyoption, and compresses + encrypts thesourceto thetargetdestination.Commands/DecryptCommand.csThis command takes a
FileInfo source(representing the path to a serializedEncryptedFileobject) and aDirectoryInfo target(defaults toCliDefaults.AppPath), as well as the globalGuid keyoption, and decrypts + decompresses thesource.dataand outputs it to thetargetdirectory.Commands/FileCommand.csThis command serves as the base for the
encryptanddecryptsub-commands, which both have access to the definedGuid keyglobal option.Program.csWith this infrastructure in place, the
Programfile initialization is really clean.At this point, this is a functional CLI app.
Tip
You can use the included
github.cssfile to follow along with the command execution that follows.Running Encrypt Command
JSON File
{ "id": "0193a2af-e0b0-75ff-a3ec-307e52782456", "name": "github", "extension": ".css", "size": 18279, "fullName": "github.css", "fileName": "github.encrypted.json", "vector": "iNuQm9TZf5ZXqwiAxWhZ/w==", "data": "iNuQm9TZf5ZXqwiAxWhZ/wAPQPC/..." }Running Decrypt Command
The
github.cssfile should be rendered at~/.scl-patternsdirectory.Runners
Having to specify a delegate
Func<>, with all of the options individually specified in the generic signature, is a bit cumbersome. The command and its state + functionality can be decoupled by defining command runner infrastructure.Runner Infrastructure
Cli/Runners/IRunner.csThe interface specifies that all implementations will define a simple
Task Execute()method.Cli/Runners/RunnerDelegate.csThis delegate signature is passed to the
@delegatefor any command executing anIRunner.Cli/Runners/RunnerCommand.csThis class provides a sub-class of
CliCommandthat passes theRunnerDelegate<I>.Calldelegate as the base constructor@delegateargument and specifies that theIgeneric type implements theIRunnerinterface.Runner Implementation
Runners/EncryptRunner.csThe arguments passed into the constructor of
EncryptRunnerare provided by the model-boudnOptionvalues defined by theCommandhierarchy that will execute theIRunnerinstance through theRunnerDelegate.Commands/EncryptCommand.csThe runner infrastructure allows the
EncryptCommandto be simplified as follows:Runners/DecryptRunner.csThe
decryptfunctionality can be moved into a runner as well:Commands/DecryptCommand.csConfiguration
Having to pass the encryption key to the commands each time is tedious. Additionally, the
getDefaultValuefactory function has to be a compile-time constant (currentlygetDefaultValue: Guid.CreateVersion7), so internally defined command state cannot be used to initialize a value read from configuration.Defining configuration initialization state on commands from which default values are derived is also not recommnded as each command is built during CLI app initialization regardless of whether it is called or not. This generates a lot of overhead and drastically slows down CLI app startup time.
To solve this, a single configuration pipeline instance can be initialized in the
CliAppclass and fed down to theBuild()method of eachCliCommand. Then, an optionalBuildConfigOptionsdelegate action can be defined onCliCommandto provide the opportunity to specify default values from configuration if the delegate is defined.The configuration pipeline that will be setup here will load, in order of least to most precedence, as follows:
~/.scl-patterns/appsettings.json.~/.scl-patterns/appsettings.{environment}.json.appsettings.jsonco-located at the execution path.appsettings.{environment}.jsonco-located at the execution path.Configuration Infrastructure
Install the following NuGet packages:
SclPatternsOptions.csDefine the values that can be extracted from configuration, as well as a helper method for retrieving the configuration object.
Cli/CliConfig.csThis class serves as the configuration pipeline that will be initialized in the
CliAppclass.Configuration Implementation
The snippets that follow illustrate changes to the existing files that are needed to implement the configuration pipeline.
Cli/ICliCommand.csThe
Buildmethod signature needs to be modified to receive aCliConfig configargument.Important
In the code block that follows, existing code has been redacted for brevity and to highlight the changes. See comments for details.
Cli/CliCommand.csTo facilitate the configuration of configuration-based options, the
Action<CliConfig>? BuildConfigOptionsdelegate is defined as a virtual property that can be overridden in sub-classes ofCliCommand.The
CliConfiginstance is fed into theBuildmethod, and passed to the call toBuildConfigOptionsif it is not null. This instance is also passed to theBuildcommand when intializing sub-commands.Cli/CliApp.csCommands/FileCommand.csBy defining the
BuildConfigOptionsdelegate, thegetDefaultValuefactory for the key option can now leverage configuration values through the providedCliConfiginstance.The
dotnet user-secretstool can be used to initialize and set theSclPatterns:CipherKeyconfiguration value:Initialize user secrets for the project:
Set the configuration value:
bash
PowerShell
Verify secret:
dotnet user-secrets list # output SclPatterns:CipherKey = 513a7b7a-4421-4dc9-b00b-11e6868c6f99Execute the help command to verify the default key value:
In addition to user-secrets, you can also specify the configuration values:
SclPatterns:CipherKeyenvironment variable.appsettings.jsonorappsettings.{environment}.jsonfile located at either:~/.scl-patterns/Sample
appsettings.json{ "SclPatterns": { "CipherKey": "019363c4-8f8f-710a-9405-889b63791e28" } }Beta Was this translation helpful? Give feedback.
All reactions