Request and Command Handlers
This article discusses how to create request and command handlers in Greyhound, including how to construct successful and error responses.
Definitions
Request handlers
The formal definition for a request handler is:
public interface IRequestHandler<TRequest, TResult> where TRequest : Request<TResult> where TResult : class{ Task<Response<TResult>> Handle(TRequest request, CancellationToken cancellationToken);}Command handlers
The formal definition for a command handler is:
public interface ICommandHandler<TCommand> where TCommand : Command{ Task<Response> Handle(TCommand command, CancellationToken cancellationToken);}Creating a Handler
1. Create the handler class
Naming
Handlers should be named identically to the Request or Command type but with the suffix Handler appended.
Protection Level
Since handlers are implementation service classes for the purposes of dependency injection, they should be internal and sealed.
Base Class
A request handler will extend from BaseRequestHandler<TRequest, TResult>, where TRequest is the request type which extends from UserRequest<TResult> and TResult is the expected result type.
A command handler will extend from BaseCommandHandler<TCommand>, where TCommand is the command type which extends from UserCommand.
Examples
2. Implement the Handle method
The Handle method is where you will write the logic to process the request or command.
Handling a Request
For a request handler, the Handle method will return a Response<TResult> where TResult is the expected result type. Luckily, you do not need to construct a Response object yourself. Instead, you can return the result (or any error inheriting from BaseError, more on that later) directly. The secret is the Response<T> type has implicit conversions defined to make this work.
internal sealed class SearchEmployeesRequestHandler : IRequestHandler<SearchEmployeesRequest, SearchResult<Employee>>{ public async Task<Response<SearchResult<Employee>>> Handle(SearchEmployeesRequest request, CancellationToken cancellationToken) { // Perform the search, etc... }}Returning a Result or Success
If a request is successful, you can return the result directly. An implicit conversions is defined on Response<T> to make this work automatically.
internal sealed class GetInfoRequestHandler : IRequestHandler<GetInfoRequest, Info>{ public async Task<Response<Info>> Handle(GetInfoRequest request, CancellationToken cancellationToken) { string info = await GetTheInfoSomehow(request.RequestedBy, cancellationToken); string? additionalInfo = await GetAdditionalInfoSomehow(cancellationToken); return new Info(info, additionalInfo); }}If a command is successful, return Response.Success.
internal sealed class AdjustEmployeeSalaryCommandHandler : ICommandHandler<AdjustEmployeeSalaryCommand>{ public async Task<Response> Handle(AdjustEmployeeSalaryCommand command, CancellationToken cancellationToken) { // Adjust the salary await AdjustTheSalarySomehow(command.EmployeeId, command.NewSalary, cancellationToken);
// Return successful response return Response.Success; }}Returning Errors
If an error occurs, you should return an error object that inherits from BaseError. This will be automatically converted to a Response object by Greyhound. There are several built-in error types, such as NotFoundError, ValidationError, and GeneralError. The Errors static class provides methods to easily create these error objects.
internal sealed class AdjustEmployeeSalaryCommandHandler(IMediator mediator, IEmployeeRepository employeeRepository) : ICommandHandler<AdjustEmployeeSalaryCommand>{ public async Task<Response> Handle(AdjustEmployeeSalaryCommand command, CancellationToken cancellationToken) { // Get the employee Response<Employee> getEmployee = await mediator.Send(new GetEmployeeByIdRequest(command.EmployeeId, command.RequestedBy), cancellationToken); if (!getEmployee.IsSuccess) return getEmployee.Error; Employee employee = getEmployee.Value;
// Validation if (employee.Salary > command.NewSalary) { return Errors.ValidationError("New salary must be greater than the current salary."); }
// Adjust the salary employee.Salary = command.NewSalary; employee = await employeeRepository.UpdateAsync(employee, cancellationToken);
// Return successful response return Response.Success; }}A very common pattern is to query for a single entity by key, and return a NotFoundError if not found. Greyhound provides an extension method Response<T> OrNotFound<T>(this T? entity, string notFoundMessage) to make this easier.
internal sealed class GetEmployeeRequestHandler : IRequestHandler<GetEmployeeRequest, Employee>{ public async Task<Response<Employee>> Handle(GetEmployeeRequest request, CancellationToken cancellationToken) { Employee? employee = await GetTheEmployeeSomehow(request.EmployeeId, cancellationToken); return employee.OrNotFound("Employee not found"); }}Unexpected Errors / Exceptions
Greyhound will call your handler inside of a try/catch block and convert any exceptions to a GeneralError object. You should not need to catch exceptions in your handler, and you should only throw exceptions from your handler for unexpected errors.
Async/Await
Note the signature of the Handle method. The return type is either Task<Response<TResult>> or Task<Response>, and there is a CancellationToken provided. This indicates that all Handle methods are asynchronous and should include the async modified keyword. The CancellationToken is provided by Greyhound and should be respected and passed to any methods that accept it.
3. Implement the CanHandle method (optional)
If you need to perform some validation to determine if the handler can (or should) process the request or command, you can implement the CanHandle method. This method should return a BaseError if the handler should not handle the request or command. The BaseError will be returned to the caller automatically. Return null if the handler can handle the request or command.
internal sealed class GetEmployeeRequestHandler : BaseRequestHandler<GetEmployeeRequest, Employee>{ protected override BaseError? CanHandle(GetEmployeeRequest request) { // Only admins can get employee info for other employees if (request.EmployeeId != request.RequestedBy.GetEmployeeId() && !request.RequestedBy.IsInRole(RoleNames.Admin)) { // Security: Return "not found" instead of an "access denied" error to prevent information leakage return Errors.NotFound("Employee not found."); }
// Check for valid employee id if (request.EmployeeId < 1) { return Errors.ValidationError("EmployeeId must be greater than 0."); }
return null; }
protected override async Task<Response<Employee>> Handling(GetEmployeeRequest request, CancellationToken cancellationToken) { // Get the employee Employee? employee = await GetTheEmployeeSomehow(request.EmployeeId, cancellationToken); return employee.OrNotFound("Employee not found"); }}