Command Architecture

Commands represent write intents — creating, updating, or deleting state. The command side of ArturRios.Mediator enforces a strict…

Commands represent write intents — creating, updating, or deleting state. The command side of ArturRios.Mediator enforces a strict one-command-one-handler rule and isolates each execution in its own DI scope.

Core Types

TypePurpose
BaseCommandAbstract base class for all command data carriers
CommandOutputAbstract base class for the result payload
ICommandHandler<TCommand, TOutput>Synchronous handler contract
ICommandHandlerAsync<TCommand, TOutput>Asynchronous handler contract
CommandMediatorResolves and invokes the registered handler

BaseCommand

public abstract class BaseCommand;

Derive from BaseCommand to create a command. Expose the data the handler needs as plain properties. Commands are simple data carriers — no business logic belongs here.

CommandOutput

public abstract class CommandOutput;

Derive from CommandOutput to describe the result of a command (for example, the identifier of the newly created entity). The mediator wraps it in DataOutput<TOutput?> before returning it to the caller.

ICommandHandler

public interface ICommandHandler<in TCommand, TOutput>
    where TCommand : BaseCommand
    where TOutput  : CommandOutput
{
    DataOutput<TOutput?> Handle(TCommand command);
}

Implement for synchronous execution. Register one implementation per <TCommand, TOutput> pair in the DI container.

ICommandHandlerAsync

public interface ICommandHandlerAsync<in TCommand, TOutput>
    where TCommand : BaseCommand
    where TOutput  : CommandOutput
{
    Task<DataOutput<TOutput?>> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

Implement for asynchronous execution. Register one implementation per <TCommand, TOutput> pair in the DI container.

CommandMediator

public class CommandMediator(IServiceScopeFactory scopeFactory)
{
    public DataOutput<TOutput?>       ExecuteCommand     <TCommand, TOutput>(TCommand command) ...
    public Task<DataOutput<TOutput?>> ExecuteCommandAsync<TCommand, TOutput>(TCommand command,
                                          CancellationToken cancellationToken = default) ...
}

For each call the mediator creates a new DI scope, resolves the matching handler, invokes it, and disposes the scope — asynchronously on the asynchronous methods, so a handler can depend on a scoped service that only implements IAsyncDisposable. This ensures scoped dependencies (e.g. DbContext) are isolated per command execution. The asynchronous method forwards the token to the handler, and an already-canceled token throws OperationCanceledException before the scope is created. A null command throws ArgumentNullException before the scope is created.


Class Diagram

classDiagram
    direction TB

    class BaseCommand {
        <<abstract>>
    }

    class CommandOutput {
        <<abstract>>
    }

    class ICommandHandler~TCommand TOutput~ {
        <<interface>>
        +Handle(command TCommand) DataOutput~TOutput?~
    }

    class ICommandHandlerAsync~TCommand TOutput~ {
        <<interface>>
        +HandleAsync(command TCommand, ct CancellationToken) Task~DataOutput~TOutput?~~
    }

    class CommandMediator {
        -IServiceScopeFactory _scopeFactory
        +ExecuteCommand(command TCommand) DataOutput~TOutput?~
        +ExecuteCommandAsync(command TCommand, ct CancellationToken) Task~DataOutput~TOutput?~~
    }

    class CommandQueryMediator {
        -CommandMediator _commandMediator
        -QueryMediator _queryMediator
        +ExecuteCommand(command TCommand) DataOutput~TOutput?~
        +ExecuteCommandAsync(command TCommand, ct CancellationToken) Task~DataOutput~TOutput?~~
    }

    ICommandHandler~TCommand TOutput~ ..> BaseCommand : constrains TCommand
    ICommandHandler~TCommand TOutput~ ..> CommandOutput : constrains TOutput
    ICommandHandlerAsync~TCommand TOutput~ ..> BaseCommand : constrains TCommand
    ICommandHandlerAsync~TCommand TOutput~ ..> CommandOutput : constrains TOutput
    CommandMediator --> ICommandHandler~TCommand TOutput~ : resolves & invokes
    CommandMediator --> ICommandHandlerAsync~TCommand TOutput~ : resolves & invokes
    CommandQueryMediator --> CommandMediator : delegates to

Sequence Diagrams

Synchronous Command Execution

sequenceDiagram
    participant Caller
    participant CommandMediator
    participant DI as DI Container (scope)
    participant Handler as ICommandHandler

    Caller->>CommandMediator: ExecuteCommand<TCommand, TOutput>(command)
    CommandMediator->>DI: CreateScope()
    CommandMediator->>DI: GetRequiredService<ICommandHandler<TCommand, TOutput>>()
    DI-->>CommandMediator: handler
    CommandMediator->>Handler: Handle(command)
    Handler-->>CommandMediator: DataOutput<TOutput?>
    CommandMediator->>DI: Dispose scope
    CommandMediator-->>Caller: DataOutput<TOutput?>

Asynchronous Command Execution

sequenceDiagram
    participant Caller
    participant CommandMediator
    participant DI as DI Container (scope)
    participant Handler as ICommandHandlerAsync

    Caller->>CommandMediator: ExecuteCommandAsync<TCommand, TOutput>(command, ct)
    CommandMediator->>DI: CreateScope()
    CommandMediator->>DI: GetRequiredService<ICommandHandlerAsync<TCommand, TOutput>>()
    DI-->>CommandMediator: handler
    CommandMediator->>Handler: HandleAsync(command, ct)
    Handler-->>CommandMediator: Task<DataOutput<TOutput?>>
    CommandMediator->>DI: Dispose scope
    CommandMediator-->>Caller: DataOutput<TOutput?>

Usage Example

Define the command and output

public class CreateProductCommand : BaseCommand
{
    public string Name  { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

public class CreateProductOutput : CommandOutput
{
    public Guid Id { get; set; }
}

Implement the handler

public class CreateProductHandler : ICommandHandlerAsync<CreateProductCommand, CreateProductOutput>
{
    private readonly IProductRepository _repository;

    public CreateProductHandler(IProductRepository repository) => _repository = repository;

    public async Task<DataOutput<CreateProductOutput?>> HandleAsync(
        CreateProductCommand command,
        CancellationToken cancellationToken = default)
    {
        var id = await _repository.InsertAsync(command.Name, command.Price);
        return DataOutput<CreateProductOutput?>.New.WithData(new CreateProductOutput { Id = id });
    }
}

Register in DI

builder.Services.AddSingleton<CommandMediator>();
builder.Services.AddScoped<ICommandHandlerAsync<CreateProductCommand, CreateProductOutput>, CreateProductHandler>();

Dispatch

var result = await mediator.ExecuteCommandAsync<CreateProductCommand, CreateProductOutput>(
    new CreateProductCommand { Name = "Widget", Price = 9.99m });

if (result.Success)
    Console.WriteLine($"Created: {result.Data!.Id}");