Cohesive.Cli is a block for building command-line applications in the same declarative style that ASP.NET Minimal APIs use for HTTP applications.
Cohesive.Cli is a CLI authoring and hosting layer that combines System.CommandLine with Cohesive.Configuration so command-line invocations become another input into the same application configuration and dependency system used by APIs, workers, and process runtimes.

- Why It Exists
- Minimal-API Style CLI Declaration
- Declarative Parameter Binding
- CLI As Configuration Input
- Parameter Conventions
- Positional Arguments
- Subcommands
- Validation And Middleware
- Dynamic Handler Binding
- Host-Backed Execution
- Run Or Fall Back
- Output
- Testing
- Integration With Cohesive.Configuration
- The CLI Role In Cohesive
Why It Exists
Many products need both a hosted application and a CLI surface:
- an ASP.NET API host;
- a worker or process host;
- a local developer tool;
- an admin or migration CLI;
- a test harness;
- a batch or training command runner.
Those surfaces often drift. CLI arguments are parsed separately from appsettings. Environment variables are interpreted differently. Runtime profiles are reimplemented. Service activation differs from the main application host.
Cohesive.Cli is designed to avoid that drift. A CLI command declares its typed configuration, parameters, validation, middleware, and handler. At invocation time, command-line values are merged into an IConfiguration pipeline, parsed through Cohesive.Configuration, and then optionally executed inside the same host and IServiceProvider graph as the rest of the application.
Minimal-API Style CLI Declaration
A Cohesive.Cli application is declared as a command tree.
The shape is intentionally close to a Minimal API application:
- declare a root app;
- map named commands;
- bind typed input;
- add middleware;
- attach validation;
- bind a handler delegate;
- run the declared command tree.
Declarative Parameter Binding
Commands are backed by typed configuration objects. Parameters can be described using ConfigurationParameterAttribute or the expression-based ConfigurationParameterOptions<T> API from Cohesive.Configuration.
Cohesive.Cli turns that configuration metadata into System.CommandLine options, accepts values from the command line, merges them with configured providers and environment variables, then uses ConfigurationParameterParser to produce the typed command object.
cohesive train \
--dataset ds_shipments_v3 \
--model logistics-encoder \
--profile local \
--poll-interval 15The command handler receives the typed configuration through CliCommandContext<TConfiguration> or directly as a dynamically bound parameter.
static Task<int> ExecuteAsync(
CliCommandContext<TrainCommandConfiguration> context,
CancellationToken ct)
{
var config = context.Configuration;
context.Output.WriteJson(new
{
config.Dataset,
config.Model,
config.Profile
});
return Task.FromResult(0);
}CLI As Configuration Input
Cohesive.Cli treats command-line options as the last input in a normal configuration pipeline.
The effective order is:
- shared application configuration providers;
- command-specific configuration providers;
- environment variables;
- command-line values.
var app = new CliApplication("Training jobs")
.WithConfiguration(builder => builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["profile"] = "local-default",
["scheduler:poll-interval"] = "15"
}))
.WithEnvironmentVariablePrefix("COHESIVE_");This means a CLI command is not separate from the application configuration model. It is an additional override source into that model.
For example, --profile azure-dev can override a default from appsettings, while COHESIVE_MODEL=logistics-encoder can still supply a value not provided on the command line.
Parameter Conventions
Parameter conventions can be applied at the application level or command level.
app.ConfigureParameters<TrainCommandConfiguration>(options =>
{
options.Map(x => x.Dataset)
.WithCliName("dataset")
.WithDescription("Dataset identifier")
.IsRequired();
options.Map(x => x.Profile)
.WithCliName("profile")
.WithAllowedValues("local", "cloud", "test")
.IsRequired();
});These conventions use the same expression-based parameter metadata system as Cohesive.Configuration. The CLI does not need a parallel option schema.
Positional Arguments
Most configuration properties become options, but a command can map a property to a positional argument when that is the natural command shape.
var stop = app.Command<StopTrainingCommandConfiguration>(
name: "stop",
description: "Stop a training run.");
stop.Argument(x => x.RunId)
.WithName("run-id")
.WithDescription("Training run identifier.");
stop.OnExecute(StopTrainingCommand.ExecuteAsync);Invocation:
cohesive stop run-123The positional argument still flows into the same configuration binding pipeline as every other parameter.
Subcommands
Command trees can be nested.
Each subcommand has its own typed configuration, parameters, validators, and handler while still inheriting applicable application-level conventions and middleware.
Validation And Middleware
Cohesive.Cli supports validation delegates and execution middleware.
Validation can return a CliValidationResult, a string, a list of strings, Task, ValueTask, or async equivalents. If validation fails, errors are written to stderr and the handler is skipped.
app.Validate<TrainCommandConfiguration>(
config => config.Dataset == "blocked"
? ["Dataset 'blocked' is not allowed."]
: []);
app.Command<TrainCommandConfiguration>("train")
.Validate(TrainingCliValidation.ValidateTrain)
.OnExecute(TrainCommand.ExecuteAsync);Middleware wraps the command handler and receives the typed command context.
app.Use<TrainCommandConfiguration>(async (context, next) =>
{
context.Output.WriteErrorLine($"Running profile '{context.Configuration.Profile}'.");
return await next(context);
});This gives CLI applications the same compositional shape as web applications: cross-cutting behavior lives in middleware, command-specific checks live in validators, and business execution lives in handlers.
Dynamic Handler Binding
Handlers can declare the parameters they need, and Cohesive.Cli resolves them from the invocation context.
Supported handler inputs include:
CliCommandContext<TConfiguration>;- custom command context types;
- the typed configuration object;
- CancellationToken;
- ParseResult;
- CliOutput;
IConfigurationorIConfigurationRoot;- services from an attached IServiceProvider.
static async Task<int> ExecuteAsync(
TrainCommandConfiguration config,
TrainingWorkflowInvoker workflow,
CliOutput output,
CancellationToken ct)
{
var result = await workflow.StartTrainingAsync(config, ct);
output.WriteJson(result);
return 0;
}This keeps handlers small and focused. They ask for the semantic inputs and services they need rather than manually unpacking parse results.
Host-Backed Execution
Cohesive.Cli can start an application host for a command invocation, attach its service provider to the command context, execute the handler, then stop the host.
app.UseHostContext<TrainCommandConfiguration, TrainingCliExecutionContext>(
createHost: context => TrainingCliHostFactory.Build(context.ConfigurationRoot),
createContext: static (context, host) =>
TrainingCliExecutionContext.Create(context, host.Services)
);A handler can then use the custom context and DI-backed services.
static Task<int> ExecuteAsync(
TrainingCliExecutionContext context,
TrainingWorkflowInvoker workflow,
CancellationToken ct)
{
return workflow.RunAsync(context.Configuration, ct);
}When the host provides an IServiceScopeFactory, Cohesive.Cli creates a per-invocation scope so scoped services behave like they do in request-oriented applications.
This is the key bridge between CLI applications and existing application infrastructure: the CLI is another entrypoint into the same configured host, not a separate runtime with a separate dependency model.
Run Or Fall Back
A single executable can support both explicit CLI commands and ordinary host startup.
return await cli.RunOrFallbackAsync(
args,
runDefaultAsync: static (remainingArgs, ct) =>
RunAspNetHostAsync(remainingArgs, ct));If the first argument matches a registered command, Cohesive.Cli handles the invocation. If not, the application can fall through to its normal ASP.NET or worker-service startup path.
That enables an executable to behave like:
app train --dataset ds_shipments_v3 --profile local
app graph validate --path shapegraph.json
app --urls http://localhost:5000The first two invocations are CLI commands. The last one starts the normal host.
Output
CliOutput provides structured access to stdout, stderr, and JSON serialization.
context.Output.WriteLine("Done.");
context.Output.WriteErrorLine("Validation failed.");
context.Output.WriteJson(summary);Invocation-specific output writers and JSON options can be supplied through CliInvocationOptions, which makes the block straightforward to test and useful for automation.
Testing
Cohesive.Cli includes a test harness for invoking a command application with captured stdout, stderr, and exit code.
var result = await CliApplicationTestHarness.InvokeAsync(
app,
["train", "--dataset", "ds_shipments_v3", "--model", "encoder", "--profile", "local"]);
Assert.Equal(0, result.ExitCode);
Assert.Contains("ds_shipments_v3", result.StandardOutput);Because command invocations are ordinary configuration and DI activations, tests can use in-memory configuration, fake hosts, fake services, and captured output without shelling out to a separate process.
Integration With Cohesive.Configuration
Cohesive.Cli is intentionally built on Cohesive.Configuration.
The same configuration metadata powers:
- generated command-line options;
- positional argument mapping;
- environment-variable binding;
- appsettings binding;
- required-value validation;
- allowed-value validation;
- TimeSpan unit conversion;
- collection parsing from repeated or comma-separated options;
- typed command configuration objects.
This keeps CLI parameters aligned with the rest of the application. A setting can be supplied by appsettings, a profile overlay, an environment variable, or a CLI option, and all paths use the same typed parser.
The CLI Role In Cohesive
Cohesive.Cli provides:
- Minimal-API-style CLI application declaration;
- System.CommandLine command, option, argument, and parsing integration;
- typed command configuration through Cohesive.Configuration;
- shared app configuration, environment variables, and command-line overrides;
- application-level and command-level parameter conventions;
- subcommands;
- validation pipelines;
- execution middleware;
- dynamic handler parameter binding;
- host-backed execution with IServiceProvider integration;
- per-invocation service scopes;
- fallback to ordinary ASP.NET or worker host startup;
- structured output and test harness support.
The result is a CLI block that lets command-line applications become first-class entrypoints into the same semantic application, configuration, and dependency system used by the rest of Cohesive.