Query, Query Handler and Query Executor¶
Query¶
The query is something that represents an operation to fetch data from some data source. The Crystal Sharp framework provides a way to create queries and handle them in a query handler. A query handler is a class that executes the actual logic of the query and returns the desired outcome. The query executor is used to pass the query to its appropriate query handler.
A query class must implement the IQuery<T>
interface; here T
is a generic return type, which is the outcome of a query. The Crystal Sharp framework provides the QueryExecutionResult<TResult>
class, which is the standard result type for CQRS-based queries and contains the execution result, errors if there are any, and the data. Following is an example of a query class:
public class FindEmployeeByCodeQuery : IQuery<QueryExecutionResult<EmployeeReadModel>>
{
public string Code { get; set; }
}
public class EmployeeReadModel
{
public string Name { get; set; }
public string Code { get; set; }
}
The code snippet above defines a query class that retrieves the employee by code and returns the QueryExecutionResult<EmployeeReadModel>
.
IMPORTANT
It is necessary for every CQRS-based query to implement the “IQuery
The QueryExeuctionResult<TResult>
class has the following properties and methods:
public bool Success { get; set; } | This property will be set to true if the query exectues successfully and without any issues, and to false otherwise. |
public IEnumerable<Error> Errors { get; set; } | Errors will be listed here if the query is not successful; otherwise, null. |
public TResult Data { get; set; } | This property contains the actual result, which could be a value or reference type. |
public static QueryExecutionResult<TResult> WithError(IEnumerable<Error> errors) | Static method, returns the QueryExecutionResult<TResult> object with errors and sets the Success property to false. |
Query Handler¶
To effectively handle the query, a handler is required. The query handler is responsible for executing the query. The following code snippet provides an example of a query handler:
public class FindEmployeeByCodeQueryHandler : QueryHandler<FindEmployeeByCodeQuery, EmployeeReadModel>
{
private readonly AppDbContext _dbContext;
public FindEmployeeByCodeQueryHandler(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public override async Task<QueryExecutionResult<EmployeeReadModel>> Handle(FindEmployeeByCodeQuery request, CancellationToken cancellationToken = default)
{
Employee employee = _dbContext.Employees.SingleOrDefaultAsync(x => x.Code == request.Code, cancellationToken).ConfigureAwait(false);
EmployeeReadModel readModel = null;
if (employee != null)
{
readModel = new()
{
Name = employee.Name,
Code = employee.Code
};
}
return await Ok(readModel);
}
}
The code snippet above illustrates the functionality of the FindEmployeeByCodeQueryHandler
class, which handles the FindEmployeeByCodeQuery
. In order for the query handler class to properly function, it must inherit from the QueryHandler<TRequest, TResponse>
class and override the Handle(TRequest request, CancellationToken cancellationToken)
method. The TRequest
parameter in the Handle
method represents the query being handled, while TResponse
represents the resulting response from the query.
The Ok
method, which is from the base class, offers two variations: Ok(TResponse)
and Ok(IEnumerable<Response>)
. When using Ok(TResponse)
, the QueryExecution<TResponse>
will be returned, whereas Ok(IEnumerable<TResponse>)
will return the QueryExecutionResult<IEnumerable<TResponse>>
.
The QueryHandler<TRequest, TResponse>
base class has a method Fail(params string[] errorMessages)
, which could be utilized if there is any validation error or failure. The following code snippet calls the Fail
method if the query has no result:
public class FindEmployeeByCodeQueryHandler : QueryHandler<FindEmployeeByCodeQuery, EmployeeReadModel>
{
private readonly AppDbContext _dbContext;
public FindEmployeeByCodeQueryHandler(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public override async Task<QueryExecutionResult<EmployeeReadModel>> Handle(FindEmployeeByCodeQuery request, CancellationToken cancellationToken = default)
{
Employee employee = _dbContext.Employees.SingleOrDefaultAsync(x => x.Code == request.Code, cancellationToken).ConfigureAwait(false);
EmployeeReadModel readModel = null;
if (employee == null)
{
return await Fail("Employee not found.");
}
readModel = new()
{
Name = employee.Name,
Code = employee.Code
};
return await Ok(readModel);
}
}
The code snippet above invokes the Fail
method from the base class if no record is found, returns the QueryExecutionResult<TResult>
with its Success
property to false
and the arguments of the Fail
method will be assigned to the Errors
property of the QueryExecutionResult<TResult>
object.
Query Executor¶
In order to execute the query, a query executor is required, which dispatches the query to its appropriate query handler. The Crystal Sharp framework provides an interface called IQueryExecutor
to execute queries. The following code snippet represents an example of a query executor:
public class EmployeeController : ControllerBase
{
private readonly IQueryExecutor _queryExecutor;
public EmployeeController(IQueryExecutor queryExecutor)
{
_queryExecutor = queryExecutor;
}
[HttpGet]
[Route("{code}")]
public async Task<ActionResult<QueryExecutionResult<EmployeeReadModel>>> GetEmployeeByCode(string code)
{
FindEmployeeByCodeQuery query = new() { Code = code };
return await _queryExecutor.Execute(query, CancellationToken.None).ConfigureAwait(false);
}
}
The code snippet above uses constructor injection to inject the IQueryExecutor
interface into EmployeeController
. In the GetEmployeeByCode
method of EmployeeController
, a new query is created and executed by the query executor.
The IQueryExecutor
interface has the following method:
public Task<QueryExecutionResult<TResult>> Execute<TResult>(IQuery<QueryExecutionResult<TResult>> query, CancellationToken cancellationToken = default) | Executes the query and returns the QueryExecutionResult<TResult> object. The parameter query could be any class that implements the interface IQuery<QueryExecutionResult<TResult>>. The parameter cancellationToken is optional. |