diff --git a/Core.sln b/Core.sln
index f2aa3abe..bfff9a72 100644
--- a/Core.sln
+++ b/Core.sln
@@ -233,6 +233,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessUtilsSampleApp", "samples\ProcessUtilsSampleApp\ProcessUtilsSampleApp.csproj", "{58E94E6E-9A9A-4E3F-ACB5-89E47CE67C39}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cli", "Cli", "{CDCF2469-1668-4EB0-A73F-692115CF6776}"
+ ProjectSection(SolutionItems) = preProject
+ source\Cli\README.md = source\Cli\README.md
+ EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.Cli.Core", "source\Cli\CreativeCoders.Cli.Core\CreativeCoders.Cli.Core.csproj", "{4E628A1A-953A-4274-B4EF-F33E943A650D}"
EndProject
diff --git a/source/CakeBuild/CreativeCoders.CakeBuild/README.md b/source/CakeBuild/CreativeCoders.CakeBuild/README.md
new file mode 100644
index 00000000..8baf9aea
--- /dev/null
+++ b/source/CakeBuild/CreativeCoders.CakeBuild/README.md
@@ -0,0 +1,149 @@
+# CreativeCoders.CakeBuild
+
+A reusable build automation framework built on [Cake Frosting](https://cakebuild.net/docs/running-builds/runners/cake-frosting) for .NET projects. Provides a fluent builder API with pre-built CI/CD tasks — clean, build, test, pack, publish, create GitHub releases, and more — so you can set up a complete build pipeline with minimal code.
+
+## Features
+
+- 🏗️ **Fluent Builder API** — Configure your build pipeline with `CakeHostBuilder` in just a few lines
+- 📦 **Pre-built Tasks** — Standard CI/CD tasks out of the box: Clean, Restore, Build, Test, Pack, Publish, NuGet Push, Code Coverage, GitHub Releases, Distribution Packages
+- ⚙️ **Settings Interfaces** — Customize task behavior by implementing strongly-typed settings interfaces
+- 🔍 **Auto-Discovery** — Automatically finds Git root, solution files, and test projects
+- 🏷️ **GitVersion Integration** — Semantic versioning via GitVersion with static fallback
+- 🐙 **GitHub Actions Support** — Log grouping and build server integration
+
+## Getting Started
+
+### Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download) or later
+
+### Setup
+
+Create a new console application and reference the `CreativeCoders.CakeBuild` package:
+
+```xml
+
+
+ Exe
+ net10.0
+
+
+
+
+```
+
+### Minimal Example
+
+```csharp
+using CreativeCoders.CakeBuild;
+
+CakeHostBuilder.Create()
+ .UseBuildContext()
+ .AddDefaultTasks()
+ .AddBuildServerIntegration()
+ .InstallTools(
+ new DotNetToolInstallation("GitVersion.Tool", "6.5.1"),
+ new DotNetToolInstallation("dotnet-reportgenerator-globaltool", "5.5.1"))
+ .Build()
+ .Run(args);
+```
+
+## Usage
+
+### Custom Build Context
+
+Extend `CakeBuildContext` and implement the settings interfaces for the tasks you want to configure:
+
+```csharp
+public class MyBuildContext(ICakeContext context) : CakeBuildContext(context),
+ IDefaultTaskSettings,
+ ICreateDistPackagesTaskSettings
+{
+ public string Copyright => $"{DateTime.Now.Year} My Company";
+
+ public string PackageProjectUrl => "https://github.com/my-org/my-repo";
+
+ public string PackageLicenseExpression => PackageLicenseExpressions.Apache20;
+
+ public string NuGetFeedUrl => "https://api.nuget.org/v3/index.json";
+
+ public IEnumerable DistPackages =>
+ [
+ new("my-app-linux-x64", "artifacts/publish/my-app/linux-x64", DistPackageFormat.TarGz),
+ new("my-app-win-x64", "artifacts/publish/my-app/win-x64", DistPackageFormat.Zip)
+ ];
+}
+```
+
+### Available Tasks
+
+All default tasks are registered via `AddDefaultTasks()` and execute in dependency order:
+
+| Task | Description | Depends On |
+|------|-------------|------------|
+| **Clean** | Removes `bin/`, `obj/`, and artifact directories | — |
+| **Restore** | Restores NuGet packages | Clean |
+| **Build** | Builds the solution with version info from GitVersion | Restore |
+| **Test** | Runs tests with optional code coverage collection | Build |
+| **CodeCoverage** | Generates coverage reports via ReportGenerator | Test |
+| **Pack** | Creates NuGet packages with metadata | Build |
+| **NuGetPush** | Pushes packages to a NuGet feed | Pack |
+| **Publish** | Publishes applications to output directories | Build |
+| **CreateDistPackages** | Creates `.tar.gz` / `.zip` distribution archives | Publish |
+| **CreateGitHubRelease** | Creates a GitHub release with assets via Octokit | — |
+
+### Settings Interfaces
+
+Each task reads its configuration from a settings interface. Implement only the ones you need:
+
+| Interface | Configures |
+|-----------|------------|
+| `ICleanTaskSettings` | Directories to clean |
+| `ITestTaskSettings` | Test projects, coverage options |
+| `ICodeCoverageTaskSettings` | Report types and file patterns |
+| `IPackTaskSettings` | Package output, metadata (URL, license, copyright) |
+| `INuGetPushTaskSettings` | Feed URL, API key, skip flag |
+| `IPublishTaskSettings` | Per-project publish configuration (runtime, self-contained) |
+| `ICreateDistPackagesTaskSettings` | Distribution package definitions and output path |
+| `ICreateGitHubReleaseTaskSettings` | Release metadata, assets, GitHub token |
+
+> [!TIP]
+> Implement `IDefaultTaskSettings` to get all standard settings interfaces in one go.
+
+### Tool Installation
+
+Register external tools via the builder:
+
+```csharp
+CakeHostBuilder.Create()
+ .InstallTools(
+ new DotNetToolInstallation("GitVersion.Tool", "6.5.1"),
+ new DotNetToolInstallation("dotnet-reportgenerator-globaltool", "5.5.1"))
+ // ...
+```
+
+### GitHub Actions Integration
+
+Enable log grouping for GitHub Actions:
+
+```csharp
+CakeHostBuilder.Create()
+ .AddBuildServerIntegration()
+ // ...
+```
+
+This registers task setup/teardown hooks that create collapsible log groups in GitHub Actions.
+
+### Generic Task Templates
+
+For advanced scenarios, use the generic task templates (`BuildTask`, `TestTask`, etc.) with a custom context type instead of the default `CakeBuildContext`:
+
+```csharp
+[TaskName("Build")]
+[IsDependentOn(typeof(RestoreTask))]
+public class MyBuildTask : BuildTask { }
+```
+
+## Sample
+
+See [`samples/CakeBuildSample`](../../../samples/CakeBuildSample) for a complete working example that demonstrates the full pipeline setup with custom context, publishing, and distribution package creation.
diff --git a/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs b/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs
index f19a08ee..b72c4b1d 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs
@@ -2,14 +2,34 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Marks a class as a CLI command and specifies the command path used to invoke it.
+///
+/// The command path segments used to invoke this command.
[AttributeUsage(AttributeTargets.Class)]
public class CliCommandAttribute(string[] commands) : Attribute
{
+ ///
+ /// Gets or sets the display name of the command.
+ ///
+ /// The display name of the command. The default is .
public string Name { get; set; } = string.Empty;
+ ///
+ /// Gets the command path segments used to invoke this command.
+ ///
+ /// An array of strings representing the command path.
public string[] Commands { get; } = Ensure.NotNull(commands);
+ ///
+ /// Gets or sets the description of the command displayed in help output.
+ ///
+ /// The description text. The default is .
public string Description { get; set; } = string.Empty;
+ ///
+ /// Gets or sets the alternative command path segments that can also invoke this command.
+ ///
+ /// An array of alternative command path segments. The default is an empty array.
public string[] AlternativeCommands { get; init; } = [];
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/CliCommandContext.cs b/source/Cli/CreativeCoders.Cli.Core/CliCommandContext.cs
index e5faf3b3..5f0eb6e3 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CliCommandContext.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CliCommandContext.cs
@@ -2,10 +2,15 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Provides the default implementation of .
+///
[PublicAPI]
public class CliCommandContext : ICliCommandContext
{
+ ///
public string[] AllArgs { get; set; } = [];
+ ///
public string[] OptionsArgs { get; set; } = [];
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/CliCommandGroupAttribute.cs b/source/Cli/CreativeCoders.Cli.Core/CliCommandGroupAttribute.cs
index d77f49ab..e48c52ac 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CliCommandGroupAttribute.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CliCommandGroupAttribute.cs
@@ -1,9 +1,22 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Defines a command group that organizes related CLI commands under a common path.
+///
+/// The command path segments that identify this group.
+/// The description of the command group displayed in help output.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class CliCommandGroupAttribute(string[] commands, string description) : Attribute
{
+ ///
+ /// Gets the command path segments that identify this group.
+ ///
+ /// An array of strings representing the group command path.
public string[] Commands { get; } = commands;
+ ///
+ /// Gets the description of the command group displayed in help output.
+ ///
+ /// The description text.
public string Description { get; } = description;
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs b/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs
index 5b326e1a..60c5cc05 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs
@@ -1,8 +1,22 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Specifies when a CLI pre-processor or post-processor should be executed.
+///
public enum CliProcessorExecutionCondition
{
+ ///
+ /// The processor is executed for every CLI invocation.
+ ///
Always,
+
+ ///
+ /// The processor is executed only when help output is displayed.
+ ///
OnlyOnHelp,
+
+ ///
+ /// The processor is executed only when a CLI command is run.
+ ///
OnlyOnCommand
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/CliResult.cs b/source/Cli/CreativeCoders.Cli.Core/CliResult.cs
index 5748f546..41a8a349 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CliResult.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CliResult.cs
@@ -2,8 +2,16 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Represents the result of a CLI application execution.
+///
+/// The exit code of the CLI execution.
[PublicAPI]
public class CliResult(int exitCode)
{
+ ///
+ /// Gets or sets the exit code of the CLI execution.
+ ///
+ /// The exit code.
public int ExitCode { get; set; } = exitCode;
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/CommandResult.cs b/source/Cli/CreativeCoders.Cli.Core/CommandResult.cs
index ca1e8e15..b18c1160 100644
--- a/source/Cli/CreativeCoders.Cli.Core/CommandResult.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/CommandResult.cs
@@ -13,15 +13,31 @@ public class CommandResult
///
public static CommandResult Success { get; } = new CommandResult();
+ ///
+ /// Initializes a new instance of the class with an exit code of 0.
+ ///
public CommandResult() { }
+ ///
+ /// Initializes a new instance of the class with the specified exit code.
+ ///
+ /// The exit code for the command result.
public CommandResult(int exitCode)
{
ExitCode = exitCode;
}
+ ///
+ /// Gets the exit code of the command execution.
+ ///
+ /// The exit code. The default is 0.
public int ExitCode { get; init; }
+ ///
+ /// Implicitly converts an integer exit code to a .
+ ///
+ /// The exit code to convert.
+ /// A representing the exit code. Returns if the exit code is 0.
public static implicit operator CommandResult(int exitCode)
=> exitCode == 0
? Success
diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliCommand.cs b/source/Cli/CreativeCoders.Cli.Core/ICliCommand.cs
index 36974fb7..dfc797a3 100644
--- a/source/Cli/CreativeCoders.Cli.Core/ICliCommand.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/ICliCommand.cs
@@ -1,12 +1,28 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Defines a CLI command that accepts options of type .
+///
+/// The type of the options passed to the command.
public interface ICliCommand
where TOptions : class
{
+ ///
+ /// Executes the CLI command asynchronously with the specified options.
+ ///
+ /// The options for the command.
+ /// A representing the outcome of the command execution.
Task ExecuteAsync(TOptions options);
}
+///
+/// Defines a CLI command without options.
+///
public interface ICliCommand
{
+ ///
+ /// Executes the CLI command asynchronously.
+ ///
+ /// A representing the outcome of the command execution.
Task ExecuteAsync();
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs
index 7348bf0c..6e188147 100644
--- a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs
@@ -2,10 +2,22 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Defines a post-processor that executes after a CLI command has completed.
+///
[PublicAPI]
public interface ICliPostProcessor
{
+ ///
+ /// Executes the post-processor asynchronously with the result of the CLI command.
+ ///
+ /// The result of the CLI command execution.
+ /// A representing the asynchronous operation.
Task ExecuteAsync(CliResult cliResult);
+ ///
+ /// Gets the condition that determines when this post-processor is executed.
+ ///
+ /// One of the enumeration values that specifies the execution condition.
CliProcessorExecutionCondition ExecutionCondition { get; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs
index 29a5bc6a..ae796b23 100644
--- a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs
+++ b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs
@@ -2,10 +2,22 @@
namespace CreativeCoders.Cli.Core;
+///
+/// Defines a pre-processor that executes before a CLI command is processed.
+///
[PublicAPI]
public interface ICliPreProcessor
{
+ ///
+ /// Executes the pre-processor asynchronously with the provided command line arguments.
+ ///
+ /// The command line arguments passed to the CLI application.
+ /// A representing the asynchronous operation.
Task ExecuteAsync(string[] args);
+ ///
+ /// Gets the condition that determines when this pre-processor is executed.
+ ///
+ /// One of the enumeration values that specifies the execution condition.
CliProcessorExecutionCondition ExecutionCondition { get; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs
index b7461b6c..42e16051 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs
@@ -1,18 +1,42 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Defines well-known exit codes used by the CLI hosting infrastructure.
+///
public static class CliExitCodes
{
+ ///
+ /// The exit code indicating successful execution.
+ ///
public const int Success = 0;
+ ///
+ /// The exit code indicating that no matching command was found for the given arguments.
+ ///
public const int CommandNotFound = int.MinValue;
+ ///
+ /// The exit code indicating that command creation failed.
+ ///
public const int CommandCreationFailed = int.MinValue + 1;
+ ///
+ /// The exit code indicating that the command result could not be determined.
+ ///
public const int CommandResultUnknown = int.MinValue + 2;
+ ///
+ /// The exit code indicating that the command options failed validation.
+ ///
public const int CommandOptionsInvalid = int.MinValue + 3;
+ ///
+ /// The exit code indicating that a pre-processor failed during execution.
+ ///
public const int PreProcessorFailed = int.MinValue + 4;
+ ///
+ /// The exit code indicating that a post-processor failed during execution.
+ ///
public const int PostProcessorFailed = int.MinValue + 5;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilder.cs
index c5da5924..d5e7feb5 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilder.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilder.cs
@@ -2,9 +2,16 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Provides a static factory for creating instances.
+///
[ExcludeFromCodeCoverage]
public static class CliHostBuilder
{
+ ///
+ /// Creates a new instance of the default .
+ ///
+ /// A new instance.
public static ICliHostBuilder Create()
{
return new DefaultCliHostBuilder();
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs
index 21bfb4f7..43a7a00b 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using CreativeCoders.Cli.Core;
-using CreativeCoders.Cli.Hosting.PreProcessors;
+using CreativeCoders.Cli.Hosting.Processors;
namespace CreativeCoders.Cli.Hosting;
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs
index f8ea4728..8ff36cef 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs
@@ -1,6 +1,13 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Holds configuration settings for the CLI host.
+///
public class CliHostSettings
{
+ ///
+ /// Gets a value indicating whether command options validation is enabled.
+ ///
+ /// if validation is enabled; otherwise, . The default is .
public bool UseValidation { get; init; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostingServiceCollectionExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostingServiceCollectionExtensions.cs
index bb66657c..05b354f7 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostingServiceCollectionExtensions.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostingServiceCollectionExtensions.cs
@@ -8,8 +8,15 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Provides extension methods for registering CLI hosting services in an .
+///
public static class CliHostingServiceCollectionExtensions
{
+ ///
+ /// Registers the required CLI hosting services in the service collection.
+ ///
+ /// The service collection to add the CLI hosting services to.
public static void AddCliHosting(this IServiceCollection services)
{
services.TryAddSingleton();
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyCommandScanner.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyCommandScanner.cs
index 2df6844b..28dbfb27 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyCommandScanner.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyCommandScanner.cs
@@ -5,10 +5,16 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Provides the default implementation of that discovers
+/// CLI commands by scanning assemblies for types decorated with .
+///
+/// The creator used to build command info from discovered types.
public class AssemblyCommandScanner(ICommandInfoCreator commandInfoCreator) : IAssemblyCommandScanner
{
private readonly ICommandInfoCreator _commandInfoCreator = Ensure.NotNull(commandInfoCreator);
+ ///
public AssemblyScanResult ScanForCommands(Assembly[] assemblies, Func? predicate = null)
{
var commandInfos = assemblies
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyScanResult.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyScanResult.cs
index 8e8c7237..7b1357a3 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyScanResult.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/AssemblyScanResult.cs
@@ -2,9 +2,20 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Represents the result of scanning assemblies for CLI commands.
+///
public class AssemblyScanResult
{
+ ///
+ /// Gets the collection of discovered command information objects.
+ ///
+ /// An enumerable of instances.
public required IEnumerable CommandInfos { get; init; }
+ ///
+ /// Gets the collection of command group attributes found in the scanned assemblies.
+ ///
+ /// An enumerable of instances.
public required IEnumerable GroupAttributes { get; init; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/CliCommandInfo.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/CliCommandInfo.cs
index 97c469f9..046a58ee 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/CliCommandInfo.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/CliCommandInfo.cs
@@ -2,11 +2,26 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Holds metadata about a registered CLI command, including its attribute, type, and options type.
+///
public class CliCommandInfo
{
+ ///
+ /// Gets the attribute that defines the command's name, path, and description.
+ ///
+ /// The applied to the command class.
public required CliCommandAttribute CommandAttribute { get; init; }
+ ///
+ /// Gets the type of the command class.
+ ///
+ /// The of the CLI command implementation.
public required Type CommandType { get; init; }
+ ///
+ /// Gets the type of the options class used by the command, if any.
+ ///
+ /// The of the options class, or if the command has no options.
public Type? OptionsType { get; init; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/CommandInfoCreator.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/CommandInfoCreator.cs
index 34c79cee..9fe8e006 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/CommandInfoCreator.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/CommandInfoCreator.cs
@@ -4,8 +4,13 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Provides the default implementation of that creates
+/// instances by inspecting the on command types.
+///
public class CommandInfoCreator : ICommandInfoCreator
{
+ ///
public CliCommandInfo? Create(Type commandType)
{
var commandAttribute = commandType.GetCustomAttribute(false);
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/IAssemblyCommandScanner.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/IAssemblyCommandScanner.cs
index ac5eebdd..708bcb28 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/IAssemblyCommandScanner.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/IAssemblyCommandScanner.cs
@@ -2,7 +2,16 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Defines a scanner that discovers CLI commands by scanning assemblies.
+///
public interface IAssemblyCommandScanner
{
+ ///
+ /// Scans the specified assemblies for CLI commands decorated with .
+ ///
+ /// The assemblies to scan for commands.
+ /// An optional predicate to filter discovered command types.
+ /// An containing the discovered commands and group attributes.
AssemblyScanResult ScanForCommands(Assembly[] assemblies, Func? predicate = null);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/ICommandInfoCreator.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/ICommandInfoCreator.cs
index c4b2d6a1..75b2597d 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/ICommandInfoCreator.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/ICommandInfoCreator.cs
@@ -1,6 +1,14 @@
namespace CreativeCoders.Cli.Hosting.Commands;
+///
+/// Defines a creator that builds instances from command types.
+///
public interface ICommandInfoCreator
{
+ ///
+ /// Creates a for the specified command type.
+ ///
+ /// The type of the CLI command to create info for.
+ /// A instance, or if the type is not a valid CLI command.
CliCommandInfo? Create(Type commandType);
}
\ No newline at end of file
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandGroupNode.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandGroupNode.cs
index debdf8f7..7523863a 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandGroupNode.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandGroupNode.cs
@@ -2,8 +2,17 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Represents a command group node in the CLI command tree that contains child commands or subgroups.
+///
+/// The name of the command group.
+/// The parent group node, or if this is a root group.
public class CliCommandGroupNode(string groupName, CliCommandGroupNode? parent)
: CliTreeNode(groupName, parent)
{
+ ///
+ /// Gets or sets the attribute that provides metadata for this command group.
+ ///
+ /// The associated with this group, or if no attribute is defined.
public CliCommandGroupAttribute? GroupAttribute { get; set; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandNode.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandNode.cs
index bbfab40b..64993e97 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandNode.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandNode.cs
@@ -2,8 +2,18 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Represents a leaf node in the CLI command tree that holds a specific command.
+///
+/// The command information associated with this node.
+/// The command name segment.
+/// The parent group node, or if this is a root command.
public class CliCommandNode(CliCommandInfo commandInfo, string command, CliCommandGroupNode? parent)
: CliTreeNode(command, parent)
{
+ ///
+ /// Gets the command information associated with this node.
+ ///
+ /// The for this command.
public CliCommandInfo CommandInfo { get; } = Ensure.NotNull(commandInfo);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandStore.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandStore.cs
index 361aca36..8506372f 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandStore.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliCommandStore.cs
@@ -4,6 +4,9 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Provides the default implementation of that organizes commands in a tree structure.
+///
public class CliCommandStore : ICliCommandStore
{
private readonly List _commands = [];
@@ -12,6 +15,7 @@ public class CliCommandStore : ICliCommandStore
private IEnumerable? _groupAttributes;
+ ///
public void AddCommands(IEnumerable commands,
IEnumerable? groupAttributes = null)
{
@@ -22,6 +26,7 @@ public void AddCommands(IEnumerable commands,
commands.ForEach(AddCommand);
}
+ ///
public FindCommandNodeResult? FindCommandGroupNode(string[] args)
{
return FindCommandGroupNode(null, _treeRootNodes, args);
@@ -55,6 +60,7 @@ public void AddCommands(IEnumerable commands,
};
}
+ ///
public FindCommandNodeResult? FindCommandNode(string[] args)
{
return FindCommandNode(_treeRootNodes, args);
@@ -151,9 +157,12 @@ private CliCommandGroupNode GetGroupNode(CliCommandGroupNode? parent, List
public IEnumerable TreeRootNodes => _treeRootNodes;
+ ///
public IEnumerable Commands => _commands;
+ ///
public IEnumerable GroupAttributes => _groupAttributes ?? [];
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliTreeNode.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliTreeNode.cs
index 54293141..337af3a8 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliTreeNode.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/CliTreeNode.cs
@@ -2,8 +2,17 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Represents a node in the CLI command tree structure.
+///
+/// The name of this tree node.
+/// The parent group node, or if this is a root node.
public class CliTreeNode(string name, CliCommandGroupNode? parent)
{
+ ///
+ /// Returns the full name path from the root to this node.
+ ///
+ /// An enumerable of name segments from root to this node.
public IEnumerable GetNamePath()
{
if (Parent != null)
@@ -17,9 +26,21 @@ public IEnumerable GetNamePath()
yield return Name;
}
+ ///
+ /// Gets the child nodes of this tree node.
+ ///
+ /// A list of child instances.
public List ChildNodes { get; } = [];
+ ///
+ /// Gets the parent group node.
+ ///
+ /// The parent , or if this is a root node.
public CliCommandGroupNode? Parent { get; } = parent;
+ ///
+ /// Gets the name of this tree node.
+ ///
+ /// The node name.
public string Name { get; } = Ensure.IsNotNullOrWhitespace(name);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/FindCommandNodeResult.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/FindCommandNodeResult.cs
index 40cd94bc..beca9037 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/FindCommandNodeResult.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/FindCommandNodeResult.cs
@@ -2,10 +2,24 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Represents the result of searching for a node in the CLI command tree.
+///
+/// The type of the tree node found.
+/// The found tree node, or if no match was found.
+/// The arguments that were not consumed during the search.
public class FindCommandNodeResult(TNode? node, string[] remainingArgs)
where TNode : CliTreeNode
{
+ ///
+ /// Gets the found tree node.
+ ///
+ /// The matched , or if no match was found.
public TNode? Node { get; } = node;
+ ///
+ /// Gets the arguments that were not consumed during the command search.
+ ///
+ /// An array of remaining argument strings.
public string[] RemainingArgs { get; } = Ensure.NotNull(remainingArgs);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/ICliCommandStore.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/ICliCommandStore.cs
index 9b32a67f..3cdcc7b5 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/ICliCommandStore.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Store/ICliCommandStore.cs
@@ -2,19 +2,49 @@
namespace CreativeCoders.Cli.Hosting.Commands.Store;
+///
+/// Defines a store for CLI commands organized in a tree structure.
+///
public interface ICliCommandStore
{
+ ///
+ /// Adds commands and optional group attributes to the store.
+ ///
+ /// The collection of command information objects to add.
+ /// An optional collection of group attributes for organizing commands.
void AddCommands(IEnumerable commands,
IEnumerable? groupAttributes =
null);
+ ///
+ /// Searches for a command group node matching the provided arguments.
+ ///
+ /// The command line arguments to match against the command tree.
+ /// A containing the matched group node and remaining args, or if no match was found.
FindCommandNodeResult? FindCommandGroupNode(string[] args);
+ ///
+ /// Searches for a command node matching the provided arguments.
+ ///
+ /// The command line arguments to match against the command tree.
+ /// A containing the matched command node and remaining args, or if no match was found.
FindCommandNodeResult? FindCommandNode(string[] args);
+ ///
+ /// Gets the root nodes of the command tree.
+ ///
+ /// An enumerable of root instances.
IEnumerable TreeRootNodes { get; }
+ ///
+ /// Gets all registered command information objects.
+ ///
+ /// An enumerable of instances.
IEnumerable Commands { get; }
+ ///
+ /// Gets all registered command group attributes.
+ ///
+ /// An enumerable of instances.
IEnumerable GroupAttributes { get; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/CliCommandStructureValidator.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/CliCommandStructureValidator.cs
index 02081c38..a1f3df56 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/CliCommandStructureValidator.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/CliCommandStructureValidator.cs
@@ -3,8 +3,13 @@
namespace CreativeCoders.Cli.Hosting.Commands.Validation;
+///
+/// Provides the default implementation of that checks
+/// for duplicate group attributes and ambiguous command definitions.
+///
public class CliCommandStructureValidator : ICliCommandStructureValidator
{
+ ///
public void Validate(ICliCommandStore commandStore)
{
// Ensure that all Group attributes commands are unique
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/ICliCommandStructureValidator.cs b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/ICliCommandStructureValidator.cs
index ec35e085..2bb7fc2a 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/ICliCommandStructureValidator.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Commands/Validation/ICliCommandStructureValidator.cs
@@ -2,7 +2,16 @@
namespace CreativeCoders.Cli.Hosting.Commands.Validation;
+///
+/// Defines a validator for the CLI command tree structure.
+///
public interface ICliCommandStructureValidator
{
+ ///
+ /// Validates the command store structure for duplicates and ambiguous commands.
+ ///
+ /// The command store to validate.
+ /// Duplicate group attributes were found.
+ /// Ambiguous command definitions were found.
void Validate(ICliCommandStore commandStore);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs
index 05ade4f4..4f6eb84d 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs
@@ -13,6 +13,16 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Provides the default implementation of that resolves and executes CLI commands
+/// with pre-processing, post-processing, help handling, and error management.
+///
+/// The console used for rendering output.
+/// The command store containing all registered commands.
+/// The service provider used for dependency resolution.
+/// The handler responsible for displaying help output.
+/// The collection of pre-processors to execute before commands.
+/// The collection of post-processors to execute after commands.
public class DefaultCliHost(
IAnsiConsole ansiConsole,
ICliCommandStore commandStore,
@@ -106,6 +116,7 @@ private async Task ValidateCommandOptionsAsync(object options)
}
}
+ ///
public async Task RunAsync(string[] args)
{
try
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs
index 20445a44..93e05914 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs
@@ -12,6 +12,9 @@
namespace CreativeCoders.Cli.Hosting;
+///
+/// Provides the default implementation of for configuring and building a CLI host.
+///
public class DefaultCliHostBuilder : ICliHostBuilder
{
private readonly List _scanAssemblies = [];
@@ -28,6 +31,7 @@ public class DefaultCliHostBuilder : ICliHostBuilder
private bool _useValidation;
+ ///
public ICliHostBuilder UseContext(Action? configure = null)
where TContext : class, ICliCommandContext
{
@@ -50,6 +54,7 @@ public ICliHostBuilder UseContext(Action?
.AddSingleton());
}
+ ///
public ICliHostBuilder ConfigureServices(Action configureServices)
{
Ensure.NotNull(configureServices);
@@ -59,6 +64,7 @@ public ICliHostBuilder ConfigureServices(Action configureSer
return this;
}
+ ///
public ICliHostBuilder ScanAssemblies(params Assembly[] assemblies)
{
_scanAssemblies.AddRange(assemblies);
@@ -66,6 +72,7 @@ public ICliHostBuilder ScanAssemblies(params Assembly[] assemblies)
return this;
}
+ ///
public ICliHostBuilder EnableHelp(params HelpCommandKind[] commandKinds)
{
_helpEnabled = true;
@@ -74,6 +81,7 @@ public ICliHostBuilder EnableHelp(params HelpCommandKind[] commandKinds)
return this;
}
+ ///
public ICliHostBuilder UseValidation(bool useValidation = true)
{
_useValidation = useValidation;
@@ -81,6 +89,7 @@ public ICliHostBuilder UseValidation(bool useValidation = true)
return this;
}
+ ///
public ICliHostBuilder SkipScanEntryAssembly(bool skipScanEntryAssembly = true)
{
_skipScanEntryAssembly = skipScanEntryAssembly;
@@ -88,6 +97,7 @@ public ICliHostBuilder SkipScanEntryAssembly(bool skipScanEntryAssembly = true)
return this;
}
+ ///
public ICliHostBuilder RegisterPreProcessor(Action? configure = null)
where T : class, ICliPreProcessor
{
@@ -106,6 +116,7 @@ public ICliHostBuilder RegisterPreProcessor(Action? configure = null)
return ConfigureServices(x => x.AddSingleton());
}
+ ///
public ICliHostBuilder RegisterPostProcessor(Action? configure = null)
where T : class, ICliPostProcessor
{
@@ -124,6 +135,7 @@ public ICliHostBuilder RegisterPostProcessor(Action? configure = null)
return ConfigureServices(x => x.AddSingleton());
}
+ ///
public ICliHostBuilder UseConfiguration(Action configure)
{
Ensure.NotNull(configure);
@@ -191,6 +203,7 @@ private void ScanEntryAssemblyIfNecessary()
}
}
+ ///
public ICliHost Build()
{
ScanEntryAssemblyIfNecessary();
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/AmbiguousCliCommandsException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/AmbiguousCliCommandsException.cs
index 2b269928..ce29a2c6 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/AmbiguousCliCommandsException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/AmbiguousCliCommandsException.cs
@@ -1,4 +1,7 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when ambiguous CLI command definitions are detected during validation.
+///
public class AmbiguousCliCommandsException()
: CliCommandStructureValidationException("Ambiguous commands found");
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs
index fef13f6f..d2dc2f4a 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs
@@ -2,11 +2,25 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when a CLI command is intentionally aborted.
+///
+/// The abort message.
+/// The exit code to return.
+/// The optional inner exception.
[PublicAPI]
public class CliCommandAbortException(string message, int exitCode, Exception? exception = null)
: CliExitException(message, exitCode, exception)
{
+ ///
+ /// Gets or sets a value indicating whether this abort is treated as an error.
+ ///
+ /// if the abort is an error; otherwise, . The default is .
public bool IsError { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether the abort message should be printed to the console.
+ ///
+ /// if the message should be printed; otherwise, . The default is .
public bool PrintMessage { get; set; } = true;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandConstructionFailedException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandConstructionFailedException.cs
index 0f45bb53..1abc7819 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandConstructionFailedException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandConstructionFailedException.cs
@@ -2,6 +2,12 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when a CLI command could not be constructed from the service provider.
+///
+/// The error message.
+/// The command line arguments that were being processed.
+/// The optional inner exception that caused the construction failure.
[PublicAPI]
public class CliCommandConstructionFailedException(
string message,
@@ -9,5 +15,9 @@ public class CliCommandConstructionFailedException(
Exception? exception = null)
: CliExitException(message, CliExitCodes.CommandCreationFailed, exception)
{
+ ///
+ /// Gets the command line arguments that were being processed when construction failed.
+ ///
+ /// An array of argument strings.
public string[] Args { get; } = args;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandGroupDuplicateException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandGroupDuplicateException.cs
index cfd7cd00..925c7df2 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandGroupDuplicateException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandGroupDuplicateException.cs
@@ -1,4 +1,7 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when duplicate command group attributes are detected during validation.
+///
public class CliCommandGroupDuplicateException()
: CliCommandStructureValidationException("Group attribute duplicates found");
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandNotFoundException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandNotFoundException.cs
index 8a63a802..1ec11171 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandNotFoundException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandNotFoundException.cs
@@ -2,9 +2,18 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when no CLI command matches the provided arguments.
+///
+/// The error message.
+/// The command line arguments that did not match any command.
[PublicAPI]
public class CliCommandNotFoundException(string message, string[] args)
: CliExitException(message, CliExitCodes.CommandNotFound)
{
+ ///
+ /// Gets the command line arguments that did not match any registered command.
+ ///
+ /// An array of unmatched argument strings.
public string[] Args { get; } = args;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandOptionsInvalidException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandOptionsInvalidException.cs
index 6deae8a3..2444deca 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandOptionsInvalidException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandOptionsInvalidException.cs
@@ -3,8 +3,16 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when command options fail validation.
+///
+/// The validation result containing the failure details.
public class CliCommandOptionsInvalidException(OptionsValidationResult validationResult)
: CliExitException("Command options are invalid", CliExitCodes.CommandOptionsInvalid)
{
+ ///
+ /// Gets the validation result containing the failure messages.
+ ///
+ /// The describing the validation failures.
public OptionsValidationResult ValidationResult { get; } = Ensure.NotNull(validationResult);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandStructureValidationException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandStructureValidationException.cs
index 7a6a67f7..4c46fe1a 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandStructureValidationException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandStructureValidationException.cs
@@ -1,3 +1,7 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when the CLI command structure validation fails.
+///
+/// The validation error message.
public class CliCommandStructureValidationException(string message) : Exception(message);
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliExitException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliExitException.cs
index 78bf96f2..51dfd6f5 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliExitException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliExitException.cs
@@ -1,7 +1,17 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents a CLI exception that causes the application to exit with a specific exit code.
+///
+/// The error message.
+/// The exit code to return.
+/// The optional inner exception.
public class CliExitException(string message, int exitCode, Exception? exception = null)
: Exception(message, exception)
{
+ ///
+ /// Gets the exit code associated with this exception.
+ ///
+ /// The exit code.
public int ExitCode { get; } = exitCode;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs
index c76898f7..9a9aa15b 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs
@@ -2,10 +2,20 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when a CLI post-processor fails during execution.
+///
+/// The post-processor that caused the failure.
+/// The error message.
+/// The inner exception that caused the failure.
public class CliPostProcessorException(
ICliPostProcessor postProcessor,
string message,
Exception? innerException) : CliExitException(message, CliExitCodes.PostProcessorFailed, innerException)
{
+ ///
+ /// Gets the post-processor that caused the failure.
+ ///
+ /// The instance that failed.
public ICliPostProcessor PostProcessor { get; } = postProcessor;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs
index a0d6ceff..0e297235 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs
@@ -2,11 +2,21 @@
namespace CreativeCoders.Cli.Hosting.Exceptions;
+///
+/// Represents an exception thrown when a CLI pre-processor fails during execution.
+///
+/// The pre-processor that caused the failure.
+/// The error message.
+/// The inner exception that caused the failure.
public class CliPreProcessorException(
ICliPreProcessor preProcessor,
string message,
Exception? innerException)
: CliExitException(message, CliExitCodes.PreProcessorFailed, innerException)
{
+ ///
+ /// Gets the pre-processor that caused the failure.
+ ///
+ /// The instance that failed.
public ICliPreProcessor PreProcessor { get; } = preProcessor;
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs
index 6c6e67db..5d8a253c 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs
@@ -9,6 +9,14 @@
namespace CreativeCoders.Cli.Hosting.Help;
+///
+/// Provides the default implementation of
+/// that renders help output to the console using Spectre.Console.
+///
+/// The help handler configuration settings.
+/// The command store containing all registered commands.
+/// The console used for rendering output.
+/// The generator for options help text.
public class CliCommandHelpHandler(
HelpHandlerSettings settings,
ICliCommandStore commandStore,
@@ -24,6 +32,7 @@ public class CliCommandHelpHandler(
private readonly HelpHandlerSettings _settings = Ensure.NotNull(settings);
+ ///
public bool ShouldPrintHelp(string[] args)
{
var lowerCaseArgs = args.Select(x => x.ToLower()).ToArray();
@@ -44,6 +53,7 @@ private static bool ShouldPrintHelpFor(HelpCommandKind helpCommandKind, string[]
};
}
+ ///
public void PrintHelp(string[] args)
{
if (args.FirstOrDefault()?.ToLower() == "help")
@@ -72,6 +82,7 @@ public void PrintHelp(string[] args)
PrintHelpFor(_commandStore.TreeRootNodes.ToList());
}
+ ///
public void PrintHelpFor(IList nodeChildNodes)
{
if (nodeChildNodes.Count == 0)
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/DisabledCommandHelpHandler.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/DisabledCommandHelpHandler.cs
index 4a6a56c1..d4780527 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Help/DisabledCommandHelpHandler.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/DisabledCommandHelpHandler.cs
@@ -3,12 +3,18 @@
namespace CreativeCoders.Cli.Hosting.Help;
+///
+/// Provides a no-op implementation of used when help is disabled.
+///
[ExcludeFromCodeCoverage]
public class DisabledCommandHelpHandler : ICliCommandHelpHandler
{
+ ///
public bool ShouldPrintHelp(string[] args) => false;
+ ///
public void PrintHelp(string[] args) { }
+ ///
public void PrintHelpFor(IList nodeChildNodes) { }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs
index 169bcfb9..e7ae8c05 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs
@@ -1,9 +1,27 @@
namespace CreativeCoders.Cli.Hosting.Help;
+///
+/// Defines the types of help command invocation supported by the CLI application.
+///
public enum HelpCommandKind
{
+ ///
+ /// Help is invoked using the help command as the first argument.
+ ///
Command,
+
+ ///
+ /// Help is invoked using the --help argument.
+ ///
Argument,
+
+ ///
+ /// Help is displayed when no arguments are provided.
+ ///
EmptyArgs,
+
+ ///
+ /// Help is invoked using either the help command or the --help argument.
+ ///
CommandOrArgument
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs
index 3a47678e..6baca5e4 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs
@@ -1,6 +1,13 @@
namespace CreativeCoders.Cli.Hosting.Help;
+///
+/// Holds the configuration settings for the help handler.
+///
public class HelpHandlerSettings
{
+ ///
+ /// Gets the help command kinds that trigger help output.
+ ///
+ /// An array of values. The default is an empty array.
public HelpCommandKind[] CommandKinds { get; init; } = [];
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/ICliCommandHelpHandler.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/ICliCommandHelpHandler.cs
index 7c4876b7..1296d88c 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/Help/ICliCommandHelpHandler.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/ICliCommandHelpHandler.cs
@@ -2,11 +2,27 @@
namespace CreativeCoders.Cli.Hosting.Help;
+///
+/// Defines a handler for printing CLI command help information.
+///
public interface ICliCommandHelpHandler
{
+ ///
+ /// Determines whether help should be printed based on the provided arguments.
+ ///
+ /// The command line arguments.
+ /// if help should be printed; otherwise, .
bool ShouldPrintHelp(string[] args);
+ ///
+ /// Prints help output based on the provided arguments.
+ ///
+ /// The command line arguments used to determine which help to display.
void PrintHelp(string[] args);
+ ///
+ /// Prints help output for the specified collection of tree nodes.
+ ///
+ /// The list of CLI tree nodes to display help for.
void PrintHelpFor(IList nodeChildNodes);
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintFooterPostProcessor.cs
similarity index 53%
rename from source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs
rename to source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintFooterPostProcessor.cs
index 1153f950..5cb4e901 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintFooterPostProcessor.cs
@@ -4,14 +4,19 @@
using JetBrains.Annotations;
using Spectre.Console;
-namespace CreativeCoders.Cli.Hosting.PreProcessors;
+namespace CreativeCoders.Cli.Hosting.Processors;
+///
+/// Prints footer lines to the console after command execution as a CLI post-processor.
+///
+/// The console used for rendering output.
[UsedImplicitly]
[ExcludeFromCodeCoverage]
public class PrintFooterPostProcessor(IAnsiConsole ansiConsole) : ICliPostProcessor
{
private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole);
+ ///
public Task ExecuteAsync(CliResult cliResult)
{
if (PlainText)
@@ -32,9 +37,18 @@ public Task ExecuteAsync(CliResult cliResult)
return Task.CompletedTask;
}
+ ///
public CliProcessorExecutionCondition ExecutionCondition { get; set; }
+ ///
+ /// Gets or sets the lines of text to display in the footer.
+ ///
+ /// An enumerable collection of footer lines. The default is an empty collection.
public IEnumerable Lines { get; set; } = [];
+ ///
+ /// Gets or sets a value indicating whether the lines are rendered as plain text.
+ ///
+ /// if the lines are rendered as plain text; otherwise, to render as markup. The default is .
public bool PlainText { get; set; }
}
diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintHeaderPreProcessor.cs
similarity index 53%
rename from source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs
rename to source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintHeaderPreProcessor.cs
index 4feefd99..2a48bf36 100644
--- a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs
+++ b/source/Cli/CreativeCoders.Cli.Hosting/Processors/PrintHeaderPreProcessor.cs
@@ -4,14 +4,19 @@
using JetBrains.Annotations;
using Spectre.Console;
-namespace CreativeCoders.Cli.Hosting.PreProcessors;
+namespace CreativeCoders.Cli.Hosting.Processors;
+///
+/// Prints header lines to the console before command execution as a CLI pre-processor.
+///
+/// The console used for rendering output.
[UsedImplicitly]
[ExcludeFromCodeCoverage]
public class PrintHeaderPreProcessor(IAnsiConsole ansiConsole) : ICliPreProcessor
{
private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole);
+ ///
public Task ExecuteAsync(string[] args)
{
if (PlainText)
@@ -32,9 +37,18 @@ public Task ExecuteAsync(string[] args)
return Task.CompletedTask;
}
+ ///
public CliProcessorExecutionCondition ExecutionCondition { get; set; }
+ ///
+ /// Gets or sets the lines of text to display in the header.
+ ///
+ /// An enumerable collection of header lines. The default is an empty collection.
public IEnumerable Lines { get; set; } = [];
+ ///
+ /// Gets or sets a value indicating whether the lines are rendered as plain text.
+ ///
+ /// if the lines are rendered as plain text; otherwise, to render as markup. The default is .
public bool PlainText { get; set; }
}
diff --git a/source/Cli/README.md b/source/Cli/README.md
new file mode 100644
index 00000000..57fdaca8
--- /dev/null
+++ b/source/Cli/README.md
@@ -0,0 +1,306 @@
+# CreativeCoders.Cli
+
+A lightweight, convention-based CLI hosting framework for .NET that lets you build command-line applications with structured commands, automatic help generation, and dependency injection — all with minimal boilerplate.
+
+## Packages
+
+| Package | Description |
+|---|---|
+| `CreativeCoders.Cli.Core` | Core abstractions: command interfaces, attributes, and result types |
+| `CreativeCoders.Cli.Hosting` | Hosting infrastructure: builder, command discovery, help, pre/post-processors |
+
+## Getting started
+
+### Installation
+
+```bash
+dotnet add package CreativeCoders.Cli.Core
+dotnet add package CreativeCoders.Cli.Hosting
+```
+
+### Minimal example
+
+Create a CLI host in your `Program.cs`:
+
+```csharp
+using CreativeCoders.Cli.Hosting;
+using CreativeCoders.Cli.Hosting.Help;
+
+await CliHostBuilder.Create()
+ .EnableHelp(HelpCommandKind.CommandOrArgument)
+ .Build()
+ .RunMainAsync(args);
+```
+
+Define a command:
+
+```csharp
+using CreativeCoders.Cli.Core;
+
+[CliCommand(["greet"], Name = "greet", Description = "Prints a greeting")]
+public class GreetCommand : ICliCommand
+{
+ public Task ExecuteAsync()
+ {
+ Console.WriteLine("Hello from the CLI!");
+ return Task.FromResult(CommandResult.Success);
+ }
+}
+```
+
+Run it:
+
+```bash
+dotnet run -- greet
+# Output: Hello from the CLI!
+
+dotnet run -- --help
+# Output: Lists all available commands
+```
+
+The hosting framework automatically discovers commands in the entry assembly by scanning for classes decorated with `[CliCommand]`.
+
+## Commands with options
+
+Commands can accept strongly-typed options that are automatically parsed from arguments:
+
+```csharp
+using CreativeCoders.SysConsole.Cli.Parsing;
+
+public class DeployOptions
+{
+ [OptionValue(0, HelpText = "The target environment")]
+ public string? Environment { get; set; }
+
+ [OptionParameter('t', "tag", HelpText = "Docker image tag", DefaultValue = "latest")]
+ public string? Tag { get; set; }
+
+ [OptionParameter('d', "dry-run", HelpText = "Preview without applying changes")]
+ public bool DryRun { get; set; }
+}
+```
+
+```csharp
+[CliCommand(["deploy"], Name = "deploy", Description = "Deploy the application")]
+public class DeployCommand : ICliCommand
+{
+ public Task ExecuteAsync(DeployOptions options)
+ {
+ Console.WriteLine($"Deploying to {options.Environment} with tag {options.Tag}");
+
+ if (options.DryRun)
+ {
+ Console.WriteLine("(dry run — no changes applied)");
+ }
+
+ return Task.FromResult(CommandResult.Success);
+ }
+}
+```
+
+```bash
+dotnet run -- deploy staging --tag v2.1.0 --dry-run
+```
+
+## Command groups
+
+Organize related commands under a common group using multi-segment command paths:
+
+```csharp
+using CreativeCoders.Cli.Core;
+
+// Register the group (assembly-level attribute)
+[assembly: CliCommandGroup(["project"], "Project management commands")]
+```
+
+```csharp
+[CliCommand(["project", "init"], Name = "init", Description = "Initialize a new project")]
+public class ProjectInitCommand : ICliCommand
+{
+ public Task ExecuteAsync()
+ {
+ Console.WriteLine("Project initialized.");
+ return Task.FromResult(CommandResult.Success);
+ }
+}
+
+[CliCommand(["project", "build"], Name = "build", Description = "Build the project")]
+public class ProjectBuildCommand : ICliCommand
+{
+ public Task ExecuteAsync()
+ {
+ Console.WriteLine("Project built.");
+ return Task.FromResult(CommandResult.Success);
+ }
+}
+```
+
+```bash
+dotnet run -- project init
+dotnet run -- project build
+dotnet run -- help project # Lists commands in the "project" group
+```
+
+## Alternative commands
+
+Commands can define aliases via `AlternativeCommands`:
+
+```csharp
+[CliCommand(["project", "init"], AlternativeCommands = ["init"])]
+public class ProjectInitCommand : ICliCommand { /* ... */ }
+```
+
+Now both `project init` and `init` invoke the same command.
+
+## Dependency injection
+
+Commands are resolved through the DI container. Register services on the builder:
+
+```csharp
+var host = CliHostBuilder.Create()
+ .EnableHelp(HelpCommandKind.CommandOrArgument)
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton();
+ })
+ .Build();
+
+await host.RunMainAsync(args);
+```
+
+Inject dependencies via primary constructors:
+
+```csharp
+[CliCommand(["deploy"], Name = "deploy", Description = "Deploy the application")]
+public class DeployCommand(IDeployService deployService) : ICliCommand
+{
+ public async Task ExecuteAsync()
+ {
+ await deployService.DeployAsync();
+ return CommandResult.Success;
+ }
+}
+```
+
+## Options validation
+
+Enable validation on the host builder and implement `IOptionsValidation` on your options class:
+
+```csharp
+var host = CliHostBuilder.Create()
+ .UseValidation()
+ .EnableHelp(HelpCommandKind.CommandOrArgument)
+ .Build();
+```
+
+```csharp
+public class DeployOptions : IOptionsValidation
+{
+ [OptionValue(0, HelpText = "The target environment")]
+ public string? Environment { get; set; }
+
+ public Task ValidateAsync()
+ {
+ if (string.IsNullOrWhiteSpace(Environment))
+ {
+ return Task.FromResult(
+ OptionsValidationResult.Invalid(["Environment is required"]));
+ }
+
+ return Task.FromResult(OptionsValidationResult.Valid());
+ }
+}
+```
+
+When validation fails, the error messages are printed to the console automatically.
+
+## Pre- and post-processors
+
+Add logic that runs before or after every command — useful for headers, footers, or telemetry:
+
+```csharp
+var host = CliHostBuilder.Create()
+ .EnableHelp(HelpCommandKind.CommandOrArgument)
+ .PrintHeaderMarkup(["[bold green]My CLI Tool v1.0[/]"])
+ .PrintFooterText(["Done."])
+ .Build();
+```
+
+For custom logic, implement `ICliPreProcessor` or `ICliPostProcessor`:
+
+```csharp
+public class TimingPreProcessor : ICliPreProcessor
+{
+ public CliProcessorExecutionCondition ExecutionCondition
+ => CliProcessorExecutionCondition.OnlyOnCommand;
+
+ public Task ExecuteAsync(string[] args)
+ {
+ Console.WriteLine($"Started at {DateTime.Now:T}");
+ return Task.CompletedTask;
+ }
+}
+```
+
+Register it on the builder:
+
+```csharp
+builder.RegisterPreProcessor();
+```
+
+The `CliProcessorExecutionCondition` controls when the processor runs:
+
+| Value | Runs when |
+|---|---|
+| `Always` | Every CLI invocation |
+| `OnlyOnCommand` | A command is executed |
+| `OnlyOnHelp` | Help output is displayed |
+
+## Help system
+
+Enable help with one or more `HelpCommandKind` values:
+
+```csharp
+builder.EnableHelp(HelpCommandKind.CommandOrArgument);
+```
+
+| Kind | Trigger |
+|---|---|
+| `Command` | `help` as the first argument |
+| `Argument` | `--help` anywhere in args |
+| `EmptyArgs` | No arguments provided |
+| `CommandOrArgument` | Either `help` or `--help` |
+
+Help output is auto-generated from command attributes and option annotations.
+
+## Configuration
+
+Add configuration sources (JSON files, environment variables, etc.) via `UseConfiguration`:
+
+```csharp
+builder.UseConfiguration(config =>
+{
+ config.AddJsonFile("appsettings.json", optional: true);
+ config.AddEnvironmentVariables();
+});
+```
+
+The resulting `IConfiguration` is available for injection in your commands.
+
+## Assembly scanning
+
+By default, the entry assembly is scanned for commands. To scan additional assemblies:
+
+```csharp
+builder.ScanAssemblies(typeof(SomeCommandInAnotherAssembly).Assembly);
+```
+
+To disable entry assembly scanning (e.g., when all commands live in external libraries):
+
+```csharp
+builder.SkipScanEntryAssembly();
+```
+
+## Full example
+
+A complete sample application is available in [`samples/CliHostSampleApp`](../../samples/CliHostSampleApp).
diff --git a/source/Core/CreativeCoders.Core/CreativeCoders.Core.csproj b/source/Core/CreativeCoders.Core/CreativeCoders.Core.csproj
index 4e33f226..8720768a 100644
--- a/source/Core/CreativeCoders.Core/CreativeCoders.Core.csproj
+++ b/source/Core/CreativeCoders.Core/CreativeCoders.Core.csproj
@@ -3,8 +3,13 @@
Library
Basic core classes and types
+ README.md
+
+
+
+
diff --git a/source/Core/CreativeCoders.Core/README.md b/source/Core/CreativeCoders.Core/README.md
new file mode 100644
index 00000000..58827941
--- /dev/null
+++ b/source/Core/CreativeCoders.Core/README.md
@@ -0,0 +1,773 @@
+# CreativeCoders.Core
+
+[](https://www.nuget.org/packages/CreativeCoders.Core)
+
+A foundational .NET library providing essential utilities, extensions, and abstractions for building robust C# applications. From parameter validation and thread-safe collections to messaging, caching, and reflection helpers — everything you need to reduce boilerplate and write cleaner code.
+
+## Installation
+
+```bash
+dotnet add package CreativeCoders.Core
+```
+
+## Features
+
+- **Parameter Validation** — Guard clauses with `Ensure` and fluent `Argument` validation
+- **Collection Extensions** — LINQ-style helpers like `ForEach`, `Distinct` by key, `WhereNotNull`, and more
+- **Thread-Safe Collections** — `ConcurrentList`, `SynchronizedValue`, and configurable locking
+- **Observable Collections** — `ExtendedObservableCollection` with batch updates and UI synchronization
+- **In-Memory Caching** — Dictionary-based cache with expiration policies and region support
+- **Chain of Responsibility** — `HandlerChain` for building processing pipelines
+- **Pub/Sub Messaging** — Loosely-coupled messenger with weak reference support
+- **Reflection Utilities** — Type discovery, generic instance creation, expression helpers
+- **String Utilities** — Case conversion, pattern matching, SecureString support, placeholder replacement
+- **Fluent Interface Helpers** — Extension methods for building fluent APIs
+- **Environment Abstraction** — Testable `Env` wrapper over `System.Environment`
+- **Visitor Pattern** — Full visitor infrastructure with sub-item traversal
+
+## Usage
+
+### Ensure — Parameter Validation
+
+The `Ensure` class provides guard clauses for method parameters with aggressive inlining for zero-overhead validation.
+
+```csharp
+using CreativeCoders.Core;
+
+public class UserService(IUserRepository repository)
+{
+ // Guard constructor-injected dependencies
+ private readonly IUserRepository _repository = Ensure.NotNull(repository);
+
+ public User GetUser(string email, Guid tenantId)
+ {
+ // Guard method arguments
+ Ensure.IsNotNullOrWhitespace(email);
+ Ensure.GuidIsNotEmpty(tenantId);
+
+ return _repository.FindByEmail(email, tenantId);
+ }
+
+ public void UpdateUsers(IEnumerable users)
+ {
+ // Guard collections against null or empty
+ Ensure.IsNotNullOrEmpty(users);
+
+ // Assert arbitrary conditions
+ Ensure.That(users.All(u => u.IsValid), "All users must be valid");
+
+ // Validate index ranges
+ Ensure.IndexIsInRange(0, users.Count());
+ }
+
+ public void LoadConfig(string configPath)
+ {
+ // Guard file system paths
+ Ensure.FileExists(configPath);
+ Ensure.DirectoryExists(Path.GetDirectoryName(configPath)!);
+ }
+}
+```
+
+### Fluent Argument Validation
+
+For a more fluent approach to parameter validation, use `Ensure.Argument`:
+
+```csharp
+using CreativeCoders.Core;
+
+public void ProcessOrder(string orderId, int quantity, string text, IEnumerable items)
+{
+ // Chain multiple checks for one argument
+ Ensure.Argument(orderId).IsNotNullOrWhitespace();
+ Ensure.Argument(items).IsNotNullOrEmpty();
+ Ensure.Argument(text).NotNull().HasMaxLength(100);
+}
+```
+
+### Object Extensions
+
+Utility extensions for safe casting, null-safe operations, and reflection-based property access.
+
+```csharp
+using CreativeCoders.Core;
+
+object value = GetValue();
+
+// Null-safe ToString
+string text = value.ToStringSafe("default");
+
+// Safe type casting with default fallback
+int number = value.As(42);
+
+// Try-pattern casting
+if (value.TryAs(out var str))
+{
+ Console.WriteLine(str);
+}
+
+// Convert public properties to a dictionary
+var user = new { Name = "Alice", Age = 30 };
+Dictionary dict = user.ToDictionary();
+
+// Reflection-based property access
+var name = someObject.GetPropertyValue("Name");
+someObject.SetPropertyValue("Name", "Bob");
+
+// Polymorphic async disposal
+await someObject.TryDisposeAsync();
+```
+
+### DelegateDisposable
+
+Create `IDisposable` and `IAsyncDisposable` instances from delegates — useful for cleanup callbacks and scope guards.
+
+```csharp
+using CreativeCoders.Core;
+
+// Create a disposable from an action
+var cleanup = new DelegateDisposable(() => Console.WriteLine("Cleaned up!"), onlyDisposeOnce: true);
+
+using (cleanup)
+{
+ // Do work...
+} // "Cleaned up!" is printed
+
+// Async variant
+var asyncCleanup = new DelegateAsyncDisposable(
+ async () => await SaveStateAsync(),
+ onlyDisposeOnce: true);
+
+await using (asyncCleanup)
+{
+ // Do work...
+}
+```
+
+### Fluent Interface Extensions
+
+Build fluent APIs by chaining operations that return `this`.
+
+```csharp
+using CreativeCoders.Core;
+
+var builder = new StringBuilder()
+ .Fluent(sb => sb.Append("Hello"))
+ .Fluent(sb => sb.Append(" World"))
+ .FluentIf(includeExclamation, sb => sb.Append("!"));
+
+// Works on any type — great for builder patterns
+var config = new ConfigBuilder()
+ .Fluent(c => c.SetTimeout(30))
+ .FluentIf(isDevelopment, c => c.EnableLogging());
+```
+
+### ObservableObject — MVVM Base Class
+
+A base class implementing `INotifyPropertyChanged` and `INotifyPropertyChanging` for MVVM scenarios.
+
+```csharp
+using CreativeCoders.Core;
+
+public class PersonViewModel : ObservableObject
+{
+ private string _name;
+ private int _age;
+
+ public string Name
+ {
+ get => _name;
+ set => Set(ref _name, value);
+ }
+
+ public int Age
+ {
+ get => _age;
+ set => Set(ref _age, value);
+ }
+}
+```
+
+### Collections — Enumerable Extensions
+
+Powerful LINQ-style extensions for everyday collection operations.
+
+```csharp
+using CreativeCoders.Core.Collections;
+
+var items = new List { "alpha", "beta", "gamma", "delta" };
+
+// ForEach with index
+items.ForEach((item, index) => Console.WriteLine($"{index}: {item}"));
+
+// Async iteration
+await items.ForEachAsync(async item => await ProcessAsync(item));
+
+// Side-effect piping (lazy — executes during enumeration)
+var processed = items
+ .Pipe(item => Log(item))
+ .Where(item => item.Length > 4)
+ .ToList();
+
+// Take elements until a condition is met (inclusive)
+var untilGamma = items.TakeUntil(x => x == "gamma"); // ["alpha", "beta", "gamma"]
+
+// Take every Nth element
+var everyOther = items.TakeEvery(2); // ["alpha", "gamma"]
+
+// Filter nulls
+var nonNull = mixedList.WhereNotNull();
+
+// Check if exactly one element matches
+bool single = items.IsSingle(x => x.StartsWith("a")); // true
+```
+
+**Distinct and duplicate detection by key:**
+
+```csharp
+var people = new List { /* ... */ };
+
+// Distinct by single key
+var uniqueByName = people.Distinct(p => p.Name);
+
+// Distinct by multiple keys
+var uniqueByNameAndAge = people.Distinct(p => p.Name, p => p.Age);
+
+// Find duplicates
+var duplicates = people.NotDistinct(p => p.Email);
+```
+
+**Multi-key sorting with direction control:**
+
+```csharp
+using CreativeCoders.Core.Comparing;
+
+var sorted = people.Sort(
+ new SortFieldInfo(p => p.LastName, SortOrder.Ascending),
+ new SortFieldInfo(p => p.Age, SortOrder.Descending));
+```
+
+**Choose — filter and transform in one step:**
+
+```csharp
+// Choose combines Where + Select
+var parsed = strings.Choose(s =>
+ int.TryParse(s, out var n) ? (true, n) : (false, 0));
+```
+
+### Collections — List and Dictionary Extensions
+
+```csharp
+using CreativeCoders.Core.Collections;
+
+// Add multiple items to any IList
+IList list = new List();
+list.AddRange(new[] { "a", "b", "c" });
+
+// Replace all items at once
+list.SetItems(new[] { "x", "y", "z" });
+
+// Reverse dictionary lookup
+var dict = new Dictionary { ["alice"] = 1, ["bob"] = 2 };
+string key = dict.GetKeyByValue(2); // "bob"
+```
+
+### Collections — ExtendedObservableCollection
+
+Thread-safe observable collection with batch update support and UI synchronization.
+
+```csharp
+using CreativeCoders.Core.Collections;
+
+var collection = new ExtendedObservableCollection();
+
+// Batch updates — raises a single CollectionChanged event
+using (collection.Update())
+{
+ collection.Add("item1");
+ collection.Add("item2");
+ collection.Add("item3");
+}
+
+// Add multiple items at once
+collection.AddRange(new[] { "item4", "item5" });
+
+// Move items with proper notifications
+collection.Move(oldIndex: 0, newIndex: 2);
+```
+
+### Caching
+
+In-memory dictionary-based cache with expiration policies and named regions.
+
+```csharp
+using CreativeCoders.Core.Caching;
+using CreativeCoders.Core.Caching.Default;
+
+// Create a cache
+var cache = CacheManager.CreateCache();
+
+// Get or add with lazy factory
+var profile = cache.GetOrAdd("user:123", () => LoadProfileFromDb("123"));
+
+// Use named regions for logical separation
+cache.AddOrUpdate("user:456", profile, regionName: "premium-users");
+
+// Try-get pattern
+if (cache.TryGet("user:123", out var cached))
+{
+ Console.WriteLine(cached.Name);
+}
+
+// Async operations
+var asyncProfile = await cache.GetOrAddAsync("user:789",
+ () => LoadProfileFromDb("789"));
+
+// Clear a specific region
+cache.Clear(regionName: "premium-users");
+
+// Remove a single entry
+cache.Remove("user:123");
+```
+
+### Chaining — Chain of Responsibility
+
+Build processing pipelines where handlers can choose to handle or pass data along.
+
+```csharp
+using CreativeCoders.Core.Chaining;
+
+// Define handlers using the delegate-based handler
+var handlers = new IChainDataHandler[]
+{
+ new ChainDataHandler(input =>
+ input.StartsWith("http")
+ ? new HandleResult(true, $"URL: {input}")
+ : new HandleResult(false, default!)),
+
+ new ChainDataHandler(input =>
+ input.Contains("@")
+ ? new HandleResult(true, $"Email: {input}")
+ : new HandleResult(false, default!)),
+
+ new ChainDataHandler(input =>
+ new HandleResult(true, $"Text: {input}"))
+};
+
+var chain = new HandlerChain(handlers);
+
+var result1 = chain.Handle("http://example.com"); // "URL: http://example.com"
+var result2 = chain.Handle("user@mail.com"); // "Email: user@mail.com"
+var result3 = chain.Handle("hello world"); // "Text: hello world"
+```
+
+### Comparing — Custom Comparers
+
+Function-based comparers for use with LINQ, sorted collections, and deduplication.
+
+```csharp
+using CreativeCoders.Core.Comparing;
+
+// Equality by key selector
+var comparer = new FuncEqualityComparer(p => p.Email);
+var unique = people.Distinct(comparer);
+
+// Sorting by key with direction
+var sorter = new FuncComparer(p => p.LastName, SortOrder.Ascending);
+var sorted = people.Order(sorter);
+
+// Combine multiple comparers — all must agree for equality
+var strict = new MultiEqualityComparer(
+ new FuncEqualityComparer(p => p.Name),
+ new FuncEqualityComparer(p => p.Age));
+```
+
+### Enums — String Conversion and Flag Enumeration
+
+```csharp
+using CreativeCoders.Core.Enums;
+
+public enum Status
+{
+ [EnumStringValue("not-started")]
+ NotStarted,
+
+ [EnumStringValue("in-progress")]
+ InProgress,
+
+ [EnumStringValue("done")]
+ Done
+}
+
+// Convert enum to its string representation
+string text = Status.InProgress.ToText(); // "in-progress"
+
+// Enumerate individual flags from a flags enum
+[Flags]
+public enum Permissions { Read = 1, Write = 2, Execute = 4 }
+
+var flags = (Permissions.Read | Permissions.Write);
+var individual = flags.EnumerateFlags(); // [Read, Write]
+```
+
+### Messaging — Pub/Sub Messenger
+
+A lightweight messenger for loosely-coupled communication between components, with weak reference support to prevent memory leaks.
+
+```csharp
+using CreativeCoders.Core.Messaging;
+
+// Use the default singleton messenger
+var messenger = Messenger.Default;
+
+// Or create an isolated instance
+var isolated = Messenger.CreateInstance();
+
+// Register a message handler (returns IDisposable for unregistration)
+var registration = messenger.Register(this, message =>
+{
+ Console.WriteLine($"Order {message.OrderId} placed!");
+});
+
+// Send a message to all registered handlers
+messenger.Send(new OrderPlacedMessage { OrderId = "ORD-001" });
+
+// Unregister via IDisposable
+registration.Dispose();
+
+// Or unregister all handlers for a receiver
+messenger.Unregister(this);
+```
+
+### Placeholders — Template String Replacement
+
+Replace named placeholders in text with configured values.
+
+```csharp
+using CreativeCoders.Core.Placeholders;
+
+var placeholders = new Dictionary
+{
+ ["Name"] = "Alice",
+ ["Role"] = "Admin",
+ ["Date"] = DateTime.Now
+};
+
+var replacer = new PlaceholderReplacer("{", "}", placeholders);
+
+string result = replacer.Replace("Hello {Name}, you are a {Role} since {Date}.");
+// "Hello Alice, you are a Admin since 4/1/2026 ..."
+
+// Replace in multiple lines at once
+var lines = new[] { "User: {Name}", "Role: {Role}" };
+var replaced = replacer.Replace(lines);
+```
+
+### Reflection — Type Discovery and Utilities
+
+```csharp
+using CreativeCoders.Core.Reflection;
+
+// Find all implementations of an interface across loaded assemblies
+IEnumerable handlers = typeof(ICommandHandler).GetImplementations();
+
+// Create a generic type instance dynamically
+object? cache = typeof(Cache<>).CreateGenericInstance(typeof(string));
+
+// Get default value for any type
+object? defaultVal = typeof(int).GetDefault(); // 0
+
+// Check if a type implements a generic interface
+bool isEnumerable = typeof(List).ImplementsGenericInterface(typeof(IEnumerable<>));
+
+// Get generic interface type arguments
+Type[] args = typeof(List).GetGenericInterfaceArguments(typeof(IEnumerable<>)); // [int]
+
+// Extract member names from expressions (useful for MVVM/reflection)
+string name = ExpressionExtensions.GetMemberName(p => p.Name); // "Name"
+```
+
+### Text — String Extensions
+
+Case conversion, pattern matching, filtering, and more.
+
+```csharp
+using CreativeCoders.Core.Text;
+
+// Null-safe string checks
+string? text = GetText();
+if (text.IsNotNullOrWhiteSpace())
+{
+ Console.WriteLine(text);
+}
+
+// Case conversions
+"myProperty".CamelCaseToPascalCase(); // "MyProperty"
+"my-component".KebabCaseToPascalCase(); // "MyComponent"
+"my_field".SnakeCaseToPascalCase(); // "MyField"
+
+// Character filtering
+"Hello, World!".Filter(char.IsLetter); // "HelloWorld"
+"user@email.com".Filter('@', '.'); // "useremailcom"
+
+// Split into key-value pair
+var kv = "Content-Type=application/json".SplitIntoKeyValue("=");
+// kv.Key == "Content-Type", kv.Value == "application/json"
+
+// Conditional StringBuilder extensions
+var sb = new StringBuilder();
+sb.AppendLineIf(includeHeader, "=== Report ===");
+sb.AppendIf(showTimestamp, $"[{DateTime.Now}] ");
+```
+
+**Pattern matching with wildcards:**
+
+```csharp
+using CreativeCoders.Core.Text;
+
+bool match1 = PatternMatcher.MatchesPattern("report.pdf", "*.pdf"); // true
+bool match2 = PatternMatcher.MatchesPattern("file01.txt", "file??.txt"); // true
+bool match3 = PatternMatcher.MatchesPattern("data.csv", "*.json"); // false
+```
+
+**Random string generation:**
+
+```csharp
+using CreativeCoders.Core.Text;
+
+string token = RandomString.Create(); // 128-byte random Base64 string
+string short_ = RandomString.Create(32); // 32-byte random Base64 string
+```
+
+### Text — JSON Serialization Abstraction
+
+```csharp
+using CreativeCoders.Core.Text.Json;
+
+IJsonSerializer serializer = new DefaultJsonSerializer();
+
+// Serialize
+string json = serializer.Serialize(new { Name = "Alice", Age = 30 });
+
+// Deserialize
+var person = serializer.Deserialize(json);
+
+// Populate an existing object (merge properties)
+var existing = new Person { Name = "Bob" };
+serializer.Populate("{\"Age\": 25}", existing);
+
+// Async stream operations
+await using var stream = File.OpenRead("data.json");
+var data = await serializer.DeserializeAsync(stream);
+```
+
+### Threading — SynchronizedValue
+
+Thread-safe wrapper for values with configurable locking strategies.
+
+```csharp
+using CreativeCoders.Core.Threading;
+
+// Simple synchronized value
+var counter = SynchronizedValue.Create(0);
+
+// Thread-safe read and write
+counter.Value = 42;
+int current = counter.Value;
+
+// Atomic update
+counter.SetValue(c => c + 1);
+
+// With custom locking mechanism
+var lockMechanism = new LockSlimLockingMechanism();
+var sharedState = SynchronizedValue.Create(lockMechanism, "initial");
+```
+
+### Threading — ConcurrentList
+
+A thread-safe `IList` implementation with configurable locking.
+
+```csharp
+using CreativeCoders.Core.Threading;
+
+var list = new ConcurrentList();
+
+// All operations are thread-safe
+list.Add("item1");
+list.Add("item2");
+bool contains = list.Contains("item1");
+int count = list.Count;
+
+// Initialize from existing collection
+var fromItems = new ConcurrentList(new[] { 1, 2, 3 });
+
+// Use custom locking (e.g., no-lock for single-threaded scenarios)
+var noLockList = new ConcurrentList(new NoLockingMechanism());
+```
+
+### Threading — Locking Mechanisms
+
+Pluggable locking strategies for thread synchronization.
+
+```csharp
+using CreativeCoders.Core.Threading;
+
+// ReaderWriterLockSlim-based (default, best for read-heavy workloads)
+ILockingMechanism rwLock = new LockSlimLockingMechanism();
+
+rwLock.Read(() =>
+{
+ // Read-locked section — multiple readers allowed
+ return cache.Get("key");
+});
+
+rwLock.Write(() =>
+{
+ // Write-locked section — exclusive access
+ cache.Set("key", "value");
+});
+
+// Simple lock(object)-based
+ILockingMechanism simpleLock = new LockLockingMechanism();
+
+// No synchronization (for single-threaded code or testing)
+ILockingMechanism noLock = new NoLockingMechanism();
+```
+
+### IO — File System Utilities
+
+Extensions for `System.IO.Abstractions` with path safety checks and directory helpers.
+
+```csharp
+using CreativeCoders.Core.IO;
+
+// Ensure directories exist
+FileSys.Directory.EnsureDirectoryExists("/data/exports");
+FileSys.Directory.EnsureDirectoryForFileNameExists("/data/exports/report.csv");
+
+// Sanitize file names
+string safe = FileSys.Path.ReplaceInvalidFileNameChars("file:name?.txt", "_");
+// "file_name_.txt"
+
+// Path safety — prevent directory traversal attacks
+bool isSafe = FileSys.Path.IsSafe("../../../etc/passwd", "/data/uploads"); // false
+FileSys.Path.EnsureSafe(userProvidedPath, allowedBasePath); // throws if unsafe
+```
+
+### SysEnvironment — Testable Environment Abstraction
+
+A drop-in replacement for `System.Environment` that can be swapped for testing.
+
+```csharp
+using CreativeCoders.Core.SysEnvironment;
+
+// Use like System.Environment
+string user = Env.UserName;
+string machine = Env.MachineName;
+string? home = Env.GetEnvironmentVariable("HOME");
+
+// Inject a mock for testing
+using (Env.SetEnvironmentImpl(mockEnvironment))
+{
+ // All Env calls now go through mockEnvironment
+ var testUser = Env.UserName; // returns mock value
+} // Original environment is automatically restored
+
+// Or use IEnvironment via DI
+services.AddEnvironment();
+
+public class MyApp(IEnvironment environment)
+{
+ public void Run()
+ {
+ Console.WriteLine($"User: {environment.UserName}");
+ Console.WriteLine($"Machine: {environment.MachineName}");
+ }
+}
+```
+
+### Weak — Weak Reference Delegates
+
+Hold references to delegates without preventing garbage collection of the owner — essential for event-based architectures.
+
+```csharp
+using CreativeCoders.Core.Weak;
+
+// Create a weak action — owner can still be GC'd
+var action = new WeakAction(myHandler.OnEvent);
+
+if (action.IsAlive())
+{
+ action.Execute();
+}
+
+// Weak function with return value
+var func = new WeakFunc(() => "result");
+string result = func.Execute();
+
+// Control owner lifetime explicitly
+var keepAlive = new WeakAction(handler.OnEvent, KeepOwnerAliveMode.KeepAlive);
+var autoGuess = new WeakAction(handler.OnEvent, KeepOwnerAliveMode.AutoGuess);
+```
+
+### Error — Error Handler Abstraction
+
+```csharp
+using CreativeCoders.Core.Error;
+
+// Delegate-based error handler
+IErrorHandler handler = new DelegateErrorHandler(ex =>
+ Console.WriteLine($"Error: {ex.Message}"));
+
+handler.HandleException(new InvalidOperationException("Something went wrong"));
+
+// Null error handler (no-op, useful as default)
+IErrorHandler nullHandler = new NullErrorHandler();
+```
+
+### Dependencies — Dependency Resolution
+
+Resolve and sort dependencies within a graph, with circular reference detection.
+
+```csharp
+using CreativeCoders.Core.Dependencies;
+
+// Build dependency graph
+var collection = new DependencyObjectCollection();
+collection.Add(new DependencyObject("app", new[] { "database", "cache" }));
+collection.Add(new DependencyObject("database", new[] { "config" }));
+collection.Add(new DependencyObject("cache", new[] { "config" }));
+collection.Add(new DependencyObject("config", Array.Empty()));
+
+// Resolve all dependencies for an element
+var resolver = new DependencyResolver(collection);
+var deps = resolver.Resolve("app"); // ["database", "cache", "config"]
+
+// Sort by dependency order
+var sorter = new DependencySorter(collection);
+var sorted = sorter.Sort(); // config, database, cache, app
+```
+
+## API Reference
+
+| Namespace | Purpose |
+|-----------|---------|
+| `CreativeCoders.Core` | Core validation (`Ensure`), extensions, disposables, fluent helpers |
+| `CreativeCoders.Core.Caching` | In-memory cache with expiration and region support |
+| `CreativeCoders.Core.Chaining` | Chain of Responsibility pattern |
+| `CreativeCoders.Core.Collections` | LINQ extensions, observable collections, list/dictionary helpers |
+| `CreativeCoders.Core.Comparing` | Function-based comparers and multi-key equality |
+| `CreativeCoders.Core.Dependencies` | Dependency graph resolution and sorting |
+| `CreativeCoders.Core.Enums` | Enum-to-string conversion and flag enumeration |
+| `CreativeCoders.Core.EnsureArguments` | Fluent argument validation with `Argument` |
+| `CreativeCoders.Core.Error` | Error handler abstraction |
+| `CreativeCoders.Core.IO` | File system extensions, path safety, directory helpers |
+| `CreativeCoders.Core.Messaging` | Pub/Sub messenger with weak references |
+| `CreativeCoders.Core.ObjectLinking` | Bi-directional property binding between objects |
+| `CreativeCoders.Core.Placeholders` | Template string placeholder replacement |
+| `CreativeCoders.Core.Reflection` | Type discovery, generic instance creation, expression utilities |
+| `CreativeCoders.Core.SysEnvironment` | Testable `System.Environment` abstraction |
+| `CreativeCoders.Core.Text` | String extensions, pattern matching, JSON serialization |
+| `CreativeCoders.Core.Threading` | Thread-safe collections, synchronized values, locking mechanisms |
+| `CreativeCoders.Core.Visitors` | Visitor pattern infrastructure |
+| `CreativeCoders.Core.Weak` | Weak reference delegates |
diff --git a/source/Core/README.md b/source/Core/README.md
deleted file mode 100644
index 3d96f671..00000000
--- a/source/Core/README.md
+++ /dev/null
@@ -1,119 +0,0 @@
-# Core .NET library
-
-Basic classes and interfaces for .NET applications.
-
-## Arguments ensuring
-Ensure arguments are as expected
-
-### Simple argument checks for null, empty, etc.
-[Ensure.cs](CreativeCoders.Core/Ensure.cs)
-```csharp
-// Argument name can be given via second parameter or via nameof. If none is given, the name of variable is used.
-Ensure.NotNull(instance); // throws ArgumentNullException if instance is null
-// Ensure.IsNullOrEmpty works for strings and IEnumerable
-Ensure.IsNullOrEmpty(str); // throws ArgumentException if str is null or empty
-Ensure.IsNullOrEmpty(items); // throws ArgumentException if items is null or empty
-Ensure.FileExists(fileName); // throws ArgumentException if fileName not exists
-Ensure.DirectoryExists(dirName); // throws ArgumentException if dirName not exists
-// for more checks see Ensure.cs
-```
-
-### More complex argument checks for null, empty, etc.
-[Ensure.Argument Extensions](CreativeCoders.Core/EnsureArguments/Extensions)
-```csharp
-// Argument name can be given via second parameter or via nameof. If none is given, the name of variable is used.
-// you can chain the checks to ensure multiple conditions at once
-Ensure.Argument(instance).NotNull(); // throws ArgumentNullException if instance is null
-Ensure.Argument(text).IsNullOrEmpty(); // throws ArgumentException if str is null or empty
-Ensure.Argument(items).IsNullOrEmpty(); // throws ArgumentException if items is null or empty
-Ensure.Argument(fileName).FileExists(); // throws ArgumentException if fileName not exists
-Ensure.Argument(dirName).DirectoryExists(); // throws ArgumentException if dirName not exists
-Ensure.Argument(text).NotNull().HasMaxLength(maxLength); // throws if text is null or exceeds max length
-```
-
-## Threading
-Thread-safe collections, synchronization primitives
-
-## Enums
-Enum extensions and helpers
-
-```csharp
-enum MyEnum
-{
- [EnumStringValue("Value1")]
- ValueOne,
- [EnumStringValue("Value2")]
- ValueTwo
-}
-
-// Instantiate a new EnumStringConverter
-var enumConverter = new EnumStringConverter();
-
-// Convert enum to string
-string enumString = enumConverter.Convert(MyEnum.ValueOne);
-Console.WriteLine(enumString); // Outputs: Value1
-
-// Convert string to enum
-MyEnum enumValue = enumConverter.Convert("Value2");
-Console.WriteLine(enumValue); // Outputs: ValueTwo
-```
-
-## Visitor pattern
-Visitor pattern implementation
-
-## IO
-Static helpers for IO operations based on System.IO.Abstractions as a replacement for File, Directory, Path, etc.
-
-## Weak action and functions
-Weak delegates for actions and functions
-
-## Reflection
-Classes and extensions for working with dynamic code
-
-## ObjectLinking
-Classes for linking objects together, so that properties of one object are automatically updated when properties of another object change
-
-## Dependency tree builder
-Classes for building and resolving dependency trees
-
-## SysEnvironment
-Abstraction for Environment class to enable mocking for unit tests
-
-#### Use static methods from Env
-```csharp
-string desktopPath = Env.GetFolderPath(Environment.SpecialFolder.Desktop);
-```
-
-#### Use IEnvironment via DI
-```csharp
-// First register service
-services.AddEnvironment();
-
-// Sample app
-public class MyApp
-{
- private readonly IEnvironment _environment;
-
- public MyApp(IEnvironment environment)
- {
- _environment = environment;
- }
-
- public void Run()
- {
- // Use the IEnvironment instance
- Console.WriteLine("Current Directory: " + _environment.CurrentDirectory);
- Console.WriteLine("Machine Name: " + _environment.MachineName);
- Console.WriteLine("User Name: " + _environment.UserName);
-
- Console.WriteLine("Environment Variables:");
- foreach (var envVar in _environment.GetEnvironmentVariables())
- {
- Console.WriteLine($"Key: {envVar.Key}, Value: {envVar.Value}");
- }
- }
-}
-```
-
-## Null objects
-Null objects for various types
diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/Commands/CliCommandStructureValidatorTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/Commands/CliCommandStructureValidatorTests.cs
index 58ca8a9f..bf9c49cf 100644
--- a/tests/CreativeCoders.Cli.Tests/Hosting/Commands/CliCommandStructureValidatorTests.cs
+++ b/tests/CreativeCoders.Cli.Tests/Hosting/Commands/CliCommandStructureValidatorTests.cs
@@ -155,6 +155,38 @@ public void Validate_WithDuplicateAlternativeCommandPaths_ThrowsException()
.ThrowExactly();
}
+ //
+ /// Ensures duplicate alternative command paths are treated as ambiguous.
+ ///
+ [Fact]
+ public void Validate_WithAlternativeCommandPaths_ThrowsNoException()
+ {
+ // Arrange
+ var commandStore = A.Fake();
+
+ A.CallTo(() => commandStore.GroupAttributes)
+ .Returns(Array.Empty());
+
+ var commands = new[]
+ {
+ CreateCommandInfo(["main", "command"], ["alias", "run"]),
+ CreateCommandInfo(["other", "command"], ["alias", "run1"])
+ };
+
+ A.CallTo(() => commandStore.Commands)
+ .Returns(commands);
+
+ var validator = new CliCommandStructureValidator();
+
+ // Act
+ var action = () => validator.Validate(commandStore);
+
+ // Assert
+ action
+ .Should()
+ .NotThrow();
+ }
+
private static CliCommandInfo CreateCommandInfo(string[] commands, string[]? alternativeCommands = null)
{
// Builds a command info instance mimicking CLI command definitions for validator inputs.