基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/rudironsoni/Synaxis --skill dotnet-cli-architecture命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
AI-powered wiki generation for code repositories with commands, agents, and skills
Routes .NET/C# work to domain skills. Loads coding-standards for code paths.
Skill manifest management for dotnet-agent-harness. Tracks skill dependencies, conflicts, version compatibility, and provides validation and resolution tools. Triggers on: skill manifest, dependency resolution, skill compatibility, version conflicts, build manifest, validate dependencies.
| name | dotnet-cli-architecture |
| description | Structures CLI app layers. Command/handler/service separation, clig.dev principles, exit codes. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
Layered CLI application architecture for .NET: command/handler/service separation following clig.dev principles, configuration precedence (appsettings → environment variables → CLI arguments), structured logging in CLI context, exit code conventions, stdin/stdout/stderr patterns, and testing CLI applications via in-process invocation with output capture.
Version assumptions: .NET 8.0+ baseline. Patterns apply to CLI tools built with System.CommandLine 2.0 and generic host.
Cross-references: [skill:dotnet-system-commandline] for System.CommandLine 2.0 API, [skill:dotnet-native-aot] for AOT publishing CLI tools, [skill:dotnet-csharp-dependency-injection] for DI patterns, [skill:dotnet-csharp-configuration] for configuration integration, [skill:dotnet-testing-strategy] for general testing patterns.
The Command Line Interface Guidelines provide language-agnostic principles for well-behaved CLI tools. These translate directly to .NET patterns.
| Principle | Implementation |
|---|---|
| Human-first output by default | Use Console.Out for data, Console.Error for diagnostics |
Machine-readable output with --json | Add a --json global option that switches output format |
| Stderr for status/diagnostics | Logging, progress bars, and prompts go to stderr |
| Stdout for data only | Piped output (mycli list | jq .) must not contain log noise |
| Non-zero exit on failure | Return specific exit codes (see conventions below) |
| Fail early, fail loudly | Validate inputs before doing work |
Respect NO_COLOR | Check Environment.GetEnvironmentVariable("NO_COLOR") |
Support --verbose and --quiet | Global options controlling output verbosity |
// Data output -- goes to stdout (can be piped)
Console.Out.WriteLine(JsonSerializer.Serialize(result, jsonContext.Options));
// Status/diagnostic output -- goes to stderr (user sees it, pipe ignores it)
Console.Error.WriteLine("Processing 42 files...");
// With ILogger (when using hosting)
// ILogger writes to stderr via console provider by default
logger.LogInformation("Connected to {Endpoint}", endpoint);
```text
---
## Layered Command → Handler → Service Architecture
Separate CLI concerns into three layers:
```bash
┌─────────────────────────────────────┐
│ Commands (System.CommandLine) │ Parse args, wire options
│ ─ RootCommand, Command, Option<T> │
├─────────────────────────────────────┤
│ Handlers (orchestration) │ Coordinate services, format output
│ ─ ICommandHandler implementations │
├─────────────────────────────────────┤
│ Services (business logic) │ Pure logic, no CLI concerns
│ ─ Interfaces + implementations │
└─────────────────────────────────────┘
```text
### Why Three Layers
- **Commands** know about CLI syntax (options, arguments, subcommands) but not business logic
- **Handlers** bridge CLI inputs to service calls and format results for output
- **Services** contain domain logic and are reusable outside the CLI (tests, libraries, APIs)
### Example Structure
```text
src/
MyCli/
MyCli.csproj
Program.cs # RootCommand + CommandLineBuilder
Commands/
SyncCommandDefinition.cs # Command, options, arguments
Handlers/
SyncHandler.cs # ICommandHandler, orchestrates services
Services/
ISyncService.cs # Business logic interface
SyncService.cs # Implementation (no CLI awareness)
Output/
ConsoleFormatter.cs # Table/JSON output formatting
```csharp
### Command Definition Layer
```csharp
// Commands/SyncCommandDefinition.cs
public static class SyncCommandDefinition
{
Option<Uri> SourceOption = (
, ) { IsRequired = };
Option<> DryRunOption = (
, );
{
command = Command(, );
command.AddOption(SourceOption);
command.AddOption(DryRunOption);
command;
}
}
```bash
```csharp
:
{
ISyncService _syncService;
ILogger<SyncHandler> _logger;
{
_syncService = syncService;
_logger = logger;
}
Uri Source { ; ; } = !;
DryRun { ; ; }
=>
InvokeAsync(context).GetAwaiter().GetResult();
{
ct = context.GetCancellationToken();
_logger.LogInformation(, Source);
result = _syncService.SyncAsync(Source, DryRun, ct);
(result.HasErrors)
{
context.Console.Error.Write();
ExitCodes.SyncFailed;
}
context.Console.Out.Write();
ExitCodes.Success;
}
}
```text
```csharp
{
;
}
:
{
HttpClient _httpClient;
{
_httpClient = httpClient;
}
{
data = _httpClient.GetFromJsonAsync<SyncData>(source, ct);
SyncResult(ItemCount: data.Items.Length);
}
}
```text
---
{Environment}.json** -- environment-specific overrides
**Environment variables** -- shell CI
**CLI arguments** -- = CommandLineBuilder(rootCommand)
.UseHost(_ => Host.CreateDefaultBuilder(), host =>
{
host.ConfigureAppConfiguration((ctx, config) =>
{
configPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
, );
(File.Exists(configPath))
{
config.AddJsonFile(configPath, optional: );
}
});
})
.UseDefaults()
.Build();
```bash
Many CLI tools support user- =>
{
logging.ClearProviders();
logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
});
```text
Map `--verbose`/`--quiet` flags to log levels:
```csharp
{
=> (verbose, quiet)
{
(, _) => LogLevel.Debug,
(_, ) => LogLevel.Warning,
_ => LogLevel.Information
};
}
host.ConfigureLogging((ctx, logging) =>
{
level = VerbosityMapping.ToLogLevel(verbose, quiet);
logging.SetMinimumLevel(level);
});
```text
---
```csharp
{
Success = ;
GeneralError = ;
InvalidUsage = ;
IoError = ;
NetworkError = ;
AuthError = ;
SyncFailed = ;
ValidationFailed = ;
}
```text
- **** = success (always)
- **** = general/unspecified error
- **** = = reserved common categories
- **+** = tool-specific error codes
- Never use exit codes > (reserved shells; = executable, = found, +N = killed signal N)
```
{
{
_service.ProcessAsync(context.GetCancellationToken());
ExitCodes.Success;
}
(HttpRequestException ex)
{
_logger.LogError(ex, );
context.Console.Error.Write();
ExitCodes.NetworkError;
}
(UnauthorizedAccessException ex)
{
context.Console.Error.Write();
ExitCodes.IoError;
}
}
```text
---
Support piped input an alternative to arguments:
```
{
input;
(InputFile )
{
input = File.ReadAllTextAsync(InputFile.FullName);
}
(Console.IsInputRedirected)
{
input = Console.In.ReadToEndAsync();
}
{
context.Console.Error.Write();
ExitCodes.InvalidUsage;
}
result = _processor.Process(input);
context.Console.Out.Write(JsonSerializer.Serialize(result));
ExitCodes.Success;
}
```json
```csharp
jsonOption = Option<>(, );
rootCommand.AddGlobalOption(jsonOption);
(useJson)
{
Console.Out.WriteLine(JsonSerializer.Serialize(result, jsonContext.Options));
}
{
ConsoleFormatter.WriteTable(result, context.Console);
}
```text
```csharp
( item _service.StreamAsync(ct))
{
Console.Error.Write();
Console.Out.WriteLine(item.ToJson());
}
Console.Error.WriteLine();
```json
---
Test the full CLI pipeline without spawning a child process:
```csharp
{
RootCommand _rootCommand;
Action<IServiceCollection>? _configureServices;
{
_rootCommand = Program.BuildRootCommand();
_configureServices = configureServices;
}
Task<( ExitCode, Stdout, Stderr)> InvokeAsync(
commandLine)
{
console = TestConsole();
builder = CommandLineBuilder(_rootCommand)
.UseHost(_ => Host.CreateDefaultBuilder(), host =>
{
(_configureServices )
{
host.ConfigureServices(_configureServices);
}
})
.UseDefaults()
.Build();
exitCode = builder.InvokeAsync(commandLine, console);
(exitCode, console.Out.ToString()!, console.Error.ToString()!);
}
}
```text
```csharp
[]
{
fakeSyncService = FakeSyncService(
SyncResult(ItemCount: ));
harness = CliTestHarness(services =>
{
services.AddSingleton<ISyncService>(fakeSyncService);
});
(exitCode, stdout, stderr) = harness.InvokeAsync(
);
Assert.Equal(, exitCode);
Assert.Contains(, stdout);
}
[]
{
harness = CliTestHarness();
(exitCode, _, stderr) = harness.InvokeAsync();
Assert.NotEqual(, exitCode);
Assert.Contains(, stderr);
}
```text
```csharp
[]
[]
[]
[]
{
harness = CliTestHarness();
(exitCode, _, _) = harness.InvokeAsync();
Assert.Equal(expectedExitCode, exitCode);
}
```text
```csharp
[]
{
harness = CliTestHarness(services =>
{
services.AddSingleton<IItemRepository>(
FakeItemRepository([ Item(, )]));
});
(exitCode, stdout, _) = harness.InvokeAsync();
Assert.Equal(, exitCode);
items = JsonSerializer.Deserialize<Item[]>(stdout);
Assert.NotNull(items);
Assert.Single(items);
}
[]
{
harness = CliTestHarness();
(_, stdout, stderr) = harness.InvokeAsync();
doc = JsonDocument.Parse(stdout);
Assert.NotNull(doc);
Assert.Contains(, stderr);
}
```text
---
**Do write diagnostic output to stdout.** Logs, progress, errors go to stderr. Stdout reserved data
output that can be piped. A CLI tool that mixes logs stdout breaks shell pipelines.
**Do hardcode exit code all errors.**