ConnyConsole is a console CLI project that uses System.CommandLine from Microsoft for argument parsing to collect some experience with this library.
- Features
- Development
- References/documentation
This chapter provides a rough overview what was implemented as examples.
Please note, some of the features listed are based on or inspired by the article series A Beginner's Guide to .NET's HostBuilder: Part 1 and following.
- Console startup implemented with Host.CreateDefaultBuilder for dependency injection, configuration loading and logging integration;
- Dependency injection configuration via
ConfigureServicesand extension method to keepProgram.cssimple;- Own extension method
AddConfigurationregisters all dependencies incl. the following:- Logger is configured via configuration injection;
- Configuration registered for options pattern usage;
- Own extension method
- Loads configuration from specific subdirectory
Configthat containsappsettings.jsonandappsettings.Development.json:- Current environment resolved from injected
HostBuilderContextto use environment specific setting file; - Intentionally
appsettings(hereloggersettings) files located in subdirectoryConfigto enforce loading them in code explicitly (as example to bypass appsettings load magic); - Allows CLI to explicitly control the loading order and prioritize the layered configuration approach (System/Global/Local) while maintaining hard-coded default values for core logic;
- Current environment resolved from injected
- Console application icon defined (check
*.csprojfile tagApplicationIcon);
- Structured logging by using **Serilog;
- Startup-logger and on startup configured based on configuration files (
loggersettings) injectable logger; - Logging on console and in file with defined format;
- Serilog can throw strange/not relatable exceptions on app startup when malformed/invalid JSON config is used;
- Serilog configuration exceptions now printed on console during startup;
- Logger configuration is stored in
loggersettings.jsonfile, because noappsettings.jsonfile is used (default configuration is hard-coded);
- Graceful and enforceable cancellation of current async executed dummy logic;
- First
[Ctrl] + [C]or[Ctrl] + [Break]initiates graceful cancellation;- Waits until logic finishes or a configurable timeout reaches and closes the application;
- Second
[Ctrl] + [C]or[Ctrl] + [Break]initiates immediate enforced cancellation;- Application exists immediately;
- First
- All that magic happens in
ConsoleCancellationTokenSourceclass; - Console cancellation event is registered in
Appclass;
- Layered configuration files approach supported;
- Possible to configure on each level the
Cancellation.Timeoutsetting (right now no other settings are supported); - In regard to the following listed configuration order, the next lower level overrides the one above (more global level):
- System-level
- Configuration file is applied to all users of the system;
- Location:
SpecialFolder.CommonApplicationData\ConnyConsole\config- Windows:
C:\ProgramData\ConnyConsole\config - Linux:
~/usr/share/ConnyConsole/config
- Windows:
- Global-level (aka User-level)
- Configuration file is applied to the user, who created it only;
- Location:
SpecialFolder.UserProfile\.connyconfig- Windows:
C:\Users\<username>\.connyconfig - Linux:
~/home/<username>/.connyconfig
- Windows:
- Local-level
- Configuration file is applied to the current working directory only;
- Location, when current working directory would be
C:\Temp- Windows:
C:\Temp\.connyconsole\config - Linux:
/c/Temp/.connyconsole/config
- Windows:
- System-level
- Flexibility
- Can have a "safe" default (like a 30-second timeout) but easily change it to 5 seconds for just one specific project without editing the global files;
- Separation of Concerns
- Distinguishes between settings that belong to machine, developer, and workspace;
- Predictable Order
- Creates clear hierarchy (see diagram Configuration override order) where most "local" or "specific" setting always wins;
| Configuration scope | Windows | Linux | Description |
|---|---|---|---|
| System | C:\ProgramData\ConnyConsole\config |
~/usr/share/ConnyConsole/config |
- Configuration file is applied to all users of the system; |
| Global (User) | C:\Users\<username>\.connyconfig |
~/home/<username>/.connyconfig |
- Configuration file is applied to the user, who created it only; |
| Local | C:\Temp\.connyconsole\config |
/c/Temp/.connyconsole/config |
- Configuration file is applied to the current working directory only; - Example working directory is C:\Temp; |
---
title: Configuration override order
---
graph TD
A[CLI]
subgraph "Highest Priority (Final)"
B[Local-level]
end
subgraph "High Priority"
C[Global / User-level]
end
subgraph "Middle Priority"
D[System-level]
end
subgraph "Lowest Priority (Base)"
E[Default Configuration
_no config files_]
end
A -->|0. Present| E
A -->|1. Load| D
A -->|2. Load| C
A -->|3. Load| B
B -->|Overwrites| C
C -->|Overwrites| D
D -->|Overwrites| E
- Configuration is stored in JSON format, according to predefined and cross-checked setting keys;
- During configuration via CLI a setting key describes its nested level using a dot '.' as a separator;
- For instance:
ConnyConfig config set Cancellation.Timeout 1s - Example above Configures
Timeoutsetting that is nested inCancellationsetting;
- For instance:
- Scope Selection: By default,
config settargets Local scope, other levels can be target using flags:-l,--local: Targets current working directory (default);-g,--global: Targets global User-level configuration;-s,--system: Targets System-level configuration;- Note: Scope flags are mutually exclusive; only one can be used at a time
ConnyConsole config set <key> <value> [[-l|--local]|[-g|--global]|[-s|--system]]
- Case-Sensitivity: Note that setting keys for the
config setcommand are case-sensitive to match the internal schema and JSON property naming; - Validation via Schema: Supported setting keys are hard-coded in a
Dictionary<string, object>within the AppSettings model;- Dictionary uses boolean values (
true) to mark leaf nodes (actual settings) and nested dictionaries to represent the hierarchy; - Before value is written, editor verifies that provided key path exists within schema;
- Dictionary uses boolean values (
- Configuration Editor: The
JsonConfigurationEditormanages the physical file I/O;- Parses existing JSON (if any), traverses the tree according to the dot-notation key, and either updates existing value or creates necessary objects to reach the new setting;
// Example: How supported configuration keys are defined in the schema
private static readonly Dictionary<string, object> SupportedSettingKeys = new()
{
["LoopOutputInterval"] = true, // Top-level setting
["Cancellation"] = new Dictionary<string, object>
{
["Timeout"] = true, // Nested setting: Cancellation.Timeout
["RetryPolicy"] = new Dictionary<string, object>
{
["Enabled"] = true // Deeply nested: Cancellation.RetryPolicy.Enabled
}
}
};/* Example: Resulting configuration file structure */
{
"LoopOutputInterval": "00:00:01",
"Cancellation": {
"Timeout": "00:00:30",
"RetryPolicy": {
"Enabled": false
}
}
}- Duration configuration more user-friendly, custom
DurationTimeParserimplemented for time-based settings (likeCancellation.Timeout); - Allows users provide intuitive values instead of raw TimeSpan strings;
- Supports various input formats such as:
- Standard format:
00:00:30or1.12:00:00(1 day, 12 hours) - Short suffixes:
500ms,10s,5m,1h,2d - Full words:
30 seconds,5 minutes,1 day - Combinations:
1h 30m,2 days 4 hours 15m 30s
- Standard format:
- Supports various input formats such as:
- For the sake of testability environment and file system abstracted in CLI code;
- File system and environment paths abstracted with TestableIO.System.IO.Abstractions.Wrappers (aka System.IO.Abstractions), unit tests can simulate System, Global, and Local directory structures without touching the actual developer's disk;
- The project uses the
EnvironmentAbstractionspackage to ensure the configuration-level providers are testable;
As an application is only as good as it was tested, this chapter gives some insights into how the console application tests were implemented.
- Unit tests are implemented with the following libraries/frameworks:
- AutoFixture.AutoNSubstitute
- AwesomeAssertions (fully community-driven fork of FluentAssertions)
- AwesomeAssertions.Analyzers (supports to use AwesomeAssertions in correct way)
- EnvironmentAbstractions.TestHelpers (mock environment values)
- NSubstitute
- NSubstitute.Analyzers.CSharp (supports to use NSubstitute in correct way)
- TestableIO.System.IO.Abstractions.TestingHelpers
- xUnit
- Graceful + enforced cancellation are tested with simulated
[Ctrl] + [C]keys pressed (ConsoleCancellationTokenSourceTests.cs); - Dependency injection extension method incl. lifetime checks;
- Lifetime check helps to notice fast if a lifetime was changed by accident or just to highlight that it was changed in general;
- Async dummy logic execution;
This chapter provides an overview of what the current build pipeline provides.
- GitVersion integrated for auto SemVer (Semantic Versioning) based on Git history;
- SonarQube (cloud, free plan) integrated;
- Build console application with
Releaseconfiguration; - Run tests;
- Collect test run results as
*.trxfile; - Collect code coverage in
OpenCoverformat, later on used to publish on SonarQube;- Done by using coverlet.msbuild + coverlet.collector (in test project only);
- Both files, the coverage file and test result file, are published as build artifacts;
- Passed, failed and skipped tests listed as part of run summary, realized with
GitHubActionsTestLoggerpackage;
- Collect test run results as
The following are some used articles listed.