Command Architecture
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
| Type | Purpose |
|---|---|
BaseCommand | Abstract base class for all command data carriers |
CommandOutput | Abstract base class for the result payload |
ICommandHandler<TCommand, TOutput> | Synchronous handler contract |
ICommandHandlerAsync<TCommand, TOutput> | Asynchronous handler contract |
CommandMediator | Resolves 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) ...
}
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, 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 toSequence 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, 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}");