Send a Request Using Mediator
Sending requests is one of the core functionalities of the mediator pattern. UnambitiousFx.Mediator provides a simple and intuitive API for sending requests and receiving responses.
Request Types
UnambitiousFx.Mediator supports two types of requests:
- Requests with a response - Implement
IRequest<TResponse>
whereTResponse
is the type of the response - Requests without a response - Implement
IRequest
(typically used for commands)
Creating a Request
To create a request, define a class or record that implements either IRequest<TResponse>
or IRequest
:
using UnambitiousFx.Mediator.Abstractions;
// Request with a response (typically a query)
public sealed record GetTodoByIdQuery : IRequest<Todo> {
public required Guid Id { get; init; }
}
// Request without a response (typically a command)
public sealed record DeleteTodoCommand : IRequest {
public required Guid Id { get; init; }
}
Creating a Request Handler
For each request, you need to create a corresponding handler that implements either IRequestHandler<TRequest, TResponse>
or IRequestHandler<TRequest>
:
using UnambitiousFx.Core;
using UnambitiousFx.Mediator;
using UnambitiousFx.Mediator.Abstractions;
// Handler for a request with a response
[RequestHandler<GetTodoByIdQuery, Todo>] // Optional: Used by Mediator.Generator
public sealed class GetTodoByIdQueryHandler : IRequestHandler<GetTodoByIdQuery, Todo> {
private readonly ITodoRepository _todoRepository;
public GetTodoByIdQueryHandler(ITodoRepository todoRepository) {
_todoRepository = todoRepository;
}
public async ValueTask<Result<Todo>> HandleAsync(
IContext context,
GetTodoByIdQuery request,
CancellationToken cancellationToken = default) {
var todo = await _todoRepository.GetByIdAsync(request.Id, cancellationToken);
if (todo == null) {
return Result<Todo>.Failure("Todo not found");
}
return Result<Todo>.Success(todo);
}
}
// Handler for a request without a response
[RequestHandler<DeleteTodoCommand>] // Optional: Used by Mediator.Generator
public sealed class DeleteTodoCommandHandler : IRequestHandler<DeleteTodoCommand> {
private readonly ITodoRepository _todoRepository;
public DeleteTodoCommandHandler(ITodoRepository todoRepository) {
_todoRepository = todoRepository;
}
public async ValueTask<Result> HandleAsync(
IContext context,
DeleteTodoCommand request,
CancellationToken cancellationToken = default) {
var deleted = await _todoRepository.DeleteAsync(request.Id, cancellationToken);
if (!deleted) {
return Result.Failure("Todo not found");
}
return Result.Success();
}
}
Sending a Request
To send a request, inject ISender
into your class and call one of the SendAsync
methods:
using UnambitiousFx.Mediator.Abstractions;
public class TodoController {
private readonly ISender _sender;
public TodoController(ISender sender) {
_sender = sender;
}
public async Task<IActionResult> GetTodo(Guid id) {
var result = await _sender.SendAsync<GetTodoByIdQuery, Todo>(
new GetTodoByIdQuery { Id = id });
return result.Match(
todo => Ok(todo),
error => NotFound(error.Message));
}
public async Task<IActionResult> DeleteTodo(Guid id) {
var result = await _sender.SendAsync(
new DeleteTodoCommand { Id = id });
return result.Match(
() => NoContent(),
error => NotFound(error.Message));
}
}
Working with Results
UnambitiousFx.Mediator uses the Result<T>
and Result
types from UnambitiousFx.Core to handle success and failure cases. These types provide a functional approach to error handling:
// Handling a Result<T>
var result = await _sender.SendAsync<GetTodoByIdQuery, Todo>(query);
result.Match(
todo => {
// Handle success case with the todo
Console.WriteLine($"Found todo: {todo.Name}");
},
error => {
// Handle error case
Console.WriteLine($"Error: {error.Message}");
});
// Handling a Result
var result = await _sender.SendAsync(command);
result.Match(
() => {
// Handle success case
Console.WriteLine("Command executed successfully");
},
error => {
// Handle error case
Console.WriteLine($"Error: {error.Message}");
});
Best Practices
- Keep requests focused: Each request should represent a single operation or query.
- Use records for requests: Records provide immutability and value-based equality, which are desirable for requests.
- Return meaningful errors: Use the
Result
type to provide clear error messages when operations fail. - Consider using CQRS: Separate commands (which modify state) from queries (which retrieve data) for better separation of concerns.