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);
}

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) ...
}

For each call the mediator creates a new DI scope, resolves the matching handler, invokes it, and disposes the scope. This ensures scoped dependencies (e.g. DbContext) are isolated per command execution.


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) Task~DataOutput~TOutput?~~
    }

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

    class CommandQueryMediator {
        -CommandMediator _commandMediator
        -QueryMediator _queryMediator
        +ExecuteCommand(command TCommand) DataOutput~TOutput?~
        +ExecuteCommandAsync(command TCommand) 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)
    CommandMediator->>DI: CreateScope()
    CommandMediator->>DI: GetRequiredService<ICommandHandlerAsync<TCommand, TOutput>>()
    DI-->>CommandMediator: handler
    CommandMediator->>Handler: HandleAsync(command)
    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)
    {
        var id = await _repository.InsertAsync(command.Name, command.Price);
        return DataOutput<CreateProductOutput?>.Success(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.IsSuccess)
    Console.WriteLine($"Created: {result.Data!.Id}");