TaskHub.Shared

Pipeline Usage & Examples

This page provides real-world examples of how to implement and use the Command Pipeline.

Basic Command and Handler

public record CreateUserCommand(string Email, string Name) : CommandBase, ICommand;

public class CreateUserHandler : ICommandHandler<CreateUserCommand, Result>
{
    public async Task<Result> HandleAsync(CreateUserCommand command, CancellationToken ct)
    {
        // Business logic here
        return ResultFactory.Success();
    }
}

Full Pipeline Example

Scenario: A command with validation, logging, result transformation, and auditing.

1. Pre-Behaviors (Validation + Logging)

[Order(1)] // Outermost
public class LoggingBehavior<TCommand, TResult>(ILogger logger) 
    : IPreBehavior<TCommand, TResult>
    where TCommand : ICommand
    where TResult : Result
{
    public async Task<TResult> HandleAsync(TCommand command, Func<TCommand, CancellationToken, Task<TResult>> next, CancellationToken ct)
    {
        logger.LogInformation("Executing command {Name}", typeof(TCommand).Name);
        var result = await next(command, ct);
        logger.LogInformation("Command {Name} finished with {Code}", typeof(TCommand).Name, result.ResultCode);
        return result;
    }
}

[Order(2)]
public class ValidationBehavior<TCommand, TResult>(IEnumerable<IValidator<TCommand>> validators) 
    : IPreBehavior<TCommand, TResult>
    where TCommand : ICommand
    where TResult : Result
{
    public async Task<TResult> HandleAsync(TCommand command, Func<TCommand, CancellationToken, Task<TResult>> next, CancellationToken ct)
    {
        var context = new ValidationContext<TCommand>(command);
        var failures = validators
            .Select(v => v.Validate(context))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
        {
            return (TResult)ResultFactory.Failed("Validation failed", 400);
        }

        return await next(command, ct);
    }
}

2. Post-Transformer (Result Enveloping)

public class EnvelopeTransformer<TCommand, TResult> 
    : IPostTransformer<TCommand, TResult>
    where TCommand : ICommand
    where TResult : Result
{
    public async Task<TResult> HandleAsync(TCommand command, TResult current, Func<TCommand, TResult, CancellationToken, Task<TResult>> next, CancellationToken ct)
    {
        // Add extra metadata to the result if needed
        return await next(command, current, ct);
    }
}

3. Post-Behavior (Audit)

public class AuditBehavior<TCommand, TResult>(IAuditService audit) 
    : IPostBehavior<TCommand, TResult>
    where TCommand : ICommand
    where TResult : Result
{
    public async Task<Result> HandleAsync(TCommand command, TResult result, CancellationToken ct)
    {
        await audit.RecordAsync(command, result);
        return ResultFactory.Success();
    }
}

Advanced: Short-circuiting with Caching

public class CachingBehavior<TCommand, TResult>(ICache cache) 
    : IPreBehavior<TCommand, TResult>
    where TCommand : ICommand
    where TResult : Result
{
    public async Task<TResult> HandleAsync(TCommand command, Func<TCommand, CancellationToken, Task<TResult>> next, CancellationToken ct)
    {
        var cacheKey = command.GetHashCode().ToString();
        if (cache.TryGet<TResult>(cacheKey, out var cachedResult))
        {
            return cachedResult;
        }

        var result = await next(command, ct);
        if (result.IsSuccess)
        {
            cache.Set(cacheKey, result);
        }
        return result;
    }
}