Command Pattern
ICommand* interfaces and Command* base classes provide a lightweight pattern for constructing injectable commands built around pipelines and middleware.
| Interface | Class | Description |
|---|---|---|
| ICommandFunction<TStart,TOutput> | CommandFunction<TStart,TOutput> | A command that takes an input and returns an output |
| ICommandFunction<TOutput> | CommandFunction<TOutput> | A command that takes no input and returns an output |
| ICommandProcedure<TStart> | CommandProcedure<TStart> | A command that takes an input and returns void |
Example 1
Example of a command that takes an input and produces an output.
public interface IMyCommand : ICommandFunction<Guid, String>
{
}
public class MyCommand : CommandFunction<Guid, String>, IMyCommand
{
public MyCommand( ILogger<GetMessageCommand> logger )
: base( logger)
{
}
protected override FunctionAsync<Guid, String> PipelineFactory()
{
return PipelineFactory
.Start<Guid>()
.WithLogging()
.Pipe( GetString )
.Build();
}
private async Task<String> GetString( IPipelineContext context, Guid id )
{
return id.ToString();
}
}
// usage
void usage( IMyCommand command )
{
var result = await command.ExecuteAsync( Guid.Create() ); // takes a Guid, returns a string
}
Example 2
Example of a command that takes no input and produces an output.
public interface IMyCommand : ICommandFunction<String>
{
}
public class MyCommand : CommandFunction<String>, IMyCommand
{
public MyCommand( ILogger<GetMessageCommand> logger )
: base( logger)
{
}
protected override FunctionAsync<Arg.Empty, String> CreatePipeline()
{
return PipelineFactory
.Start<Arg.Empty>()
.WithLogging()
.PipeAsync( GetString )
.Build();
}
private String GetString( IPipelineContext context, Arg.Empty _ )
{
return "Hello";
}
}
// usage
void usage( IMyCommand command )
{
var result = await command.ExecuteAsync(); // returns "Hello"
}
Example 3
Example of a command that takes an input and produces no output.
public interface IMyCommand : ICommandProcedure<String>
{
}
public class MyCommand : CommandProcedure<String>, IMyCommand
{
public GetCommand( ILogger<MyCommand> logger )
: base( logger)
{
}
protected override ProcedureAsync<String> CreatePipeline()
{
return PipelineFactory
.Start<String>()
.WithLogging()
.PipeAsync( ExecSomeAction )
.BuildAsProcedure();
}
private String ExecSomeAction( IPipelineContext context, String who )
{
return $"Hello {who}";
}
}
// usage
void usage( IMyCommand command )
{
var result = await command.ExecuteAsync( "me" ); // returns "Hello me"
}
Example 4
Example of a command using PipelineFactory.Create with an injected IPipelineMiddlewareProvider to automatically apply cross-cutting hooks and wraps.
public interface IDeleteSubscriptionCommand : ICommandFunction<string, DeleteOutput>
{
}
public class DeleteSubscriptionCommand : CommandFunction<string, DeleteOutput>, IDeleteSubscriptionCommand
{
private readonly IPipelineMiddlewareProvider _middlewareProvider;
public DeleteSubscriptionCommand(
IPipelineMiddlewareProvider middlewareProvider,
IPipelineContextFactory pipelineContextFactory,
ILogger<DeleteSubscriptionCommand> logger )
: base( pipelineContextFactory, logger )
{
_middlewareProvider = middlewareProvider;
}
protected override FunctionAsync<string, DeleteOutput> CreatePipeline()
{
return PipelineFactory.Create<string, DeleteOutput>( _middlewareProvider, builder =>
builder
.Pipe( ValidateId )
.PipeAsync( LoadSubscriptionAsync )
.ValidateAsync()
.PipeAsync( DeleteSubscriptionAsync )
.Pipe( Result )
);
}
// ... step methods
}
The Create method applies the provider’s hooks after Start and wraps before Build, so every command that uses the provider gets consistent middleware without any extra boilerplate. See Middleware for more details on IPipelineMiddlewareProvider.
Composing Commands into Pipelines
Commands expose their inner pipeline delegate via the PipelineFunction property. This allows one command’s pipeline to directly compose another command’s pipeline as a step, without calling ExecuteAsync. The key benefit is that the pipeline context flows through naturally – shared state, middleware, exception handling, and cancellation are all preserved.
PipeAsync with Commands
Use PipeAsync to transform the pipeline value through a command’s pipeline.
public interface IFormatCommand : ICommandFunction<string, string> { }
public class FormatCommand : CommandFunction<string, string>, IFormatCommand
{
protected override FunctionAsync<string, string> CreatePipeline()
{
return PipelineFactory
.Start<string>()
.Pipe( ( ctx, arg ) => $"[{arg}]" )
.Build();
}
}
public class ParentCommand : CommandFunction<string, string>, IParentCommand
{
private readonly IFormatCommand _formatCommand;
public ParentCommand( IFormatCommand formatCommand, /* ... */ )
{
_formatCommand = formatCommand;
}
protected override FunctionAsync<string, string> CreatePipeline()
{
return PipelineFactory
.Start<string>()
.Pipe( ( ctx, arg ) => arg.ToUpper() )
.PipeAsync( _formatCommand ) // compose the command's pipeline directly
.Build();
}
}
// "hello" -> "HELLO" -> "[HELLO]"
CallAsync with Commands
Use CallAsync to run a command’s pipeline for side effects while preserving the current value.
public interface ILogCommand : ICommandProcedure<string> { }
// In a pipeline:
.CallAsync( _logCommand ) // runs the command, preserves input value
.Pipe( ( ctx, arg ) => arg + " done" )
PipeIf and CallIf with Commands
Conditionally compose a command’s pipeline based on a runtime condition.
.PipeIf( ( ctx, arg ) => arg.Length > 5, _formatCommand )
.CallIf( ( ctx, arg ) => arg.StartsWith( "log:" ), _logCommand )
Implicit Conversion
CommandFunction and CommandProcedure define implicit conversion operators to their respective delegate types. When you have a concrete command reference, the implicit conversion allows direct use anywhere a FunctionAsync or ProcedureAsync is expected.
CommandFunction<string, string> command = /* ... */;
FunctionAsync<string, string> function = command; // implicit conversion