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

© Stillpoint Software.

Hyperbee Pipeline Docs