Skip to content

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