While the mediator can be used directly with IRequest<TResponse>, many applications prefer a CQRS-style separation between commands (writes) and queries (reads). ICommand<TResponse> and IQuery<TResponse> provide semantic clarity while still flowing through the same mediator pipeline.
ICommand<TResponse> represents operations that modify application state (writes).IQuery<TResponse> represents read-only operations.IRequest<TResponse> and flow through the same pipeline.CQRS markers enable pipeline behaviors to target commands and queries differently — for example, wrapping commands in transactions while applying caching only to queries.
Commands express intent to change state. Define a command using ICommand<TResponse>. Handlers can implement either IRequestHandler or the semantic alias ICommandHandler:
public record CreateUser(string Name) : ICommand<Guid>;
public class CreateUserHandler : ICommandHandler<CreateUser, Guid>
{
public ValueTask<Guid> Handle(CreateUser request, CancellationToken ct)
{
var id = Guid.NewGuid();
return new ValueTask<Guid>(id);
}
}
Send a command:
var userId = await mediator.Send(new CreateUser("Alice"));
Queries retrieve data without side effects. Define a query using IQuery<TResponse>. Handlers can implement either IRequestHandler or the semantic alias IQueryHandler:
public record GetUser(Guid Id) : IQuery<UserDto>;
public class GetUserHandler : IQueryHandler<GetUser, UserDto>
{
public ValueTask<UserDto> Handle(GetUser request, CancellationToken ct)
{
return new ValueTask<UserDto>(new UserDto(request.Id, "Alice"));
}
}
Send a query:
var user = await mediator.Send(new GetUser(userId));
ICommand is a non-generic marker interface implemented by all commands. It allows pipeline behaviors to detect write operations at runtime without requiring open generic pattern matching.
Pipeline behaviors can inspect the request type at runtime using the non-generic marker interfaces ICommand and IQuery:
public class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TRequest request,
IRequestHandler<TRequest, TResponse> next,
CancellationToken ct)
{
if (request is ICommand)
{
// begin transaction
}
return await next.Handle(request, ct);
}
}
This pattern enables clean separation of cross-cutting concerns:
ICommand markerIQuery<T> + HybridCache