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
-
System.CommandLine Helpful Patterns
This document, and the corresponding repository, illustrate patterns that I've found to be useful when working with the
System.CommandLine
library. 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
Guid
as 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.
IConfiguration
instance.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.sln
file: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/src
and 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.cs
All
CliCommand
instances will define aBuild
function that returns aCommand
.Cli/CliCommand.cs
A
CliCommand
is initialized with all of the state needed to generate aCommand
through theBuild
method.options
defines all of the options purely needed for this command.globals
defines all of the options that should be available from this command down to its deepest sub-commands.Sub-commands are added to the returned
Command
by selecting the result of their ownBuild
function.Cli/CliApp.cs
The
CliApp
class adds all of its direct commands by selecting the result of theirBuild
method and adding it to theRootCommand
.globals
is used to define any options that shuold be globally available from this level down to the deepest sub-command.CliDefaults.cs
The
CliDefaults
static 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 key
option.Models/EncryptedFile.cs
This 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.cs
This command takes a
FileInfo source
(required) and aDirectoryInfo target
(default:CliDefaults.AppPath
), as well as the globalGuid key
option, and compresses + encrypts thesource
to thetarget
destination.Commands/DecryptCommand.cs
This command takes a
FileInfo source
(representing the path to a serializedEncryptedFile
object) and aDirectoryInfo target
(defaults toCliDefaults.AppPath
), as well as the globalGuid key
option, and decrypts + decompresses thesource.data
and outputs it to thetarget
directory.Commands/FileCommand.cs
This command serves as the base for the
encrypt
anddecrypt
sub-commands, which both have access to the definedGuid key
global option.Program.cs
With this infrastructure in place, the
Program
file initialization is really clean.At this point, this is a functional CLI app.
Tip
You can use the included
github.css
file to follow along with the command execution that follows.Running Encrypt Command
JSON File
Running Decrypt Command
The
github.css
file should be rendered at~/.scl-patterns
directory.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.cs
The interface specifies that all implementations will define a simple
Task Execute()
method.Cli/Runners/RunnerDelegate.cs
This delegate signature is passed to the
@delegate
for any command executing anIRunner
.Cli/Runners/RunnerCommand.cs
This class provides a sub-class of
CliCommand
that passes theRunnerDelegate<I>.Call
delegate as the base constructor@delegate
argument and specifies that theI
generic type implements theIRunner
interface.Runner Implementation
Runners/EncryptRunner.cs
The arguments passed into the constructor of
EncryptRunner
are provided by the model-boudnOption
values defined by theCommand
hierarchy that will execute theIRunner
instance through theRunnerDelegate
.Commands/EncryptCommand.cs
The runner infrastructure allows the
EncryptCommand
to be simplified as follows:Runners/DecryptRunner.cs
The
decrypt
functionality can be moved into a runner as well:Commands/DecryptCommand.cs
Configuration
Having to pass the encryption key to the commands each time is tedious. Additionally, the
getDefaultValue
factory 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
CliApp
class and fed down to theBuild()
method of eachCliCommand
. Then, an optionalBuildConfigOptions
delegate action can be defined onCliCommand
to 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.json
co-located at the execution path.appsettings.{environment}.json
co-located at the execution path.Configuration Infrastructure
Install the following NuGet packages:
SclPatternsOptions.cs
Define the values that can be extracted from configuration, as well as a helper method for retrieving the configuration object.
Cli/CliConfig.cs
This class serves as the configuration pipeline that will be initialized in the
CliApp
class.Configuration Implementation
The snippets that follow illustrate changes to the existing files that are needed to implement the configuration pipeline.
Cli/ICliCommand.cs
The
Build
method signature needs to be modified to receive aCliConfig config
argument.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.cs
To facilitate the configuration of configuration-based options, the
Action<CliConfig>? BuildConfigOptions
delegate is defined as a virtual property that can be overridden in sub-classes ofCliCommand
.The
CliConfig
instance is fed into theBuild
method, and passed to the call toBuildConfigOptions
if it is not null. This instance is also passed to theBuild
command when intializing sub-commands.Cli/CliApp.cs
Commands/FileCommand.cs
By defining the
BuildConfigOptions
delegate, thegetDefaultValue
factory for the key option can now leverage configuration values through the providedCliConfig
instance.The
dotnet user-secrets
tool can be used to initialize and set theSclPatterns:CipherKey
configuration 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-11e6868c6f99
Execute the help command to verify the default key value:
In addition to user-secrets, you can also specify the configuration values:
SclPatterns:CipherKey
environment variable.appsettings.json
orappsettings.{environment}.json
file located at either:~/.scl-patterns/
Sample
appsettings.json
Beta Was this translation helpful? Give feedback.
All reactions