-
Notifications
You must be signed in to change notification settings - Fork 12
Query Module
The Query Module provides the infrastructure for implementing the "Query" side of Command Query Separation (CQS). Queries are messages that retrieve data from the system without modifying its state.
- Data Retrieval: Queries are used to read and retrieve data. They should be free of side effects.
- Single Handler: Each query must be handled by exactly one handler. LiteBus will throw an exception if zero or multiple handlers are found.
-
Naming Convention: Queries should be named with descriptive nouns that represent the data being requested (e.g.,
GetProductByIdQuery
,FindUsersByRole
).
LiteBus provides two interfaces for defining queries, both of which require a result.
This is the standard interface for queries that return a single, awaitable result.
/// <summary>
/// A query to retrieve a single product by its unique identifier.
/// </summary>
public sealed class GetProductByIdQuery : IQuery<ProductDto>
{
public required Guid ProductId { get; init; }
}
Use this interface for queries that return a stream of results via IAsyncEnumerable<T>
. This is highly efficient for:
- Large result sets that should not be loaded into memory at once.
- Real-time data feeds.
- Implementing server-side pagination.
/// <summary>
/// A query that streams all products matching a search term.
/// </summary>
public sealed class StreamProductsBySearchQuery : IStreamQuery<ProductDto>
{
public required string SearchTerm { get; init; }
}
Query handlers contain the logic to fetch data from a data source.
-
IQueryHandler<TQuery, TQueryResult>
: For queries returning a single result. -
IStreamQueryHandler<TQuery, TQueryResult>
: For queries returning a stream of results.
// Handler for a single-result query
public sealed class GetProductByIdQueryHandler : IQueryHandler<GetProductByIdQuery, ProductDto>
{
public async Task<ProductDto> HandleAsync(GetProductByIdQuery query, CancellationToken cancellationToken = default)
{
// Logic to fetch product from a repository...
}
}
// Handler for a stream-result query
public sealed class StreamProductsBySearchQueryHandler : IStreamQueryHandler<StreamProductsBySearchQuery, ProductDto>
{
public async IAsyncEnumerable<ProductDto> StreamAsync(StreamProductsBySearchQuery query, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Logic to stream products from a data source...
await foreach (var product in _repository.SearchAsync(query.SearchTerm, cancellationToken))
{
yield return new ProductDto { ... };
}
}
}
The IQueryMediator
is used to execute queries and retrieve their results.
public interface IQueryMediator
{
Task<TQueryResult> QueryAsync<TQueryResult>(IQuery<TQueryResult> query, QueryMediationSettings? settings = null, CancellationToken cancellationToken = default);
IAsyncEnumerable<TQueryResult> StreamAsync<TQueryResult>(IStreamQuery<TQueryResult> query, QueryMediationSettings? settings = null, CancellationToken cancellationToken = default);
}
// In a controller or service
public class ProductsController : ControllerBase
{
private readonly IQueryMediator _queryMediator;
public ProductsController(IQueryMediator queryMediator)
{
_queryMediator = queryMediator;
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(Guid id)
{
// Execute a single-result query
var product = await _queryMediator.QueryAsync(new GetProductByIdQuery { ProductId = id });
return Ok(product);
}
[HttpGet("search")]
public IAsyncEnumerable<ProductDto> Search([FromQuery] string term)
{
// Execute a stream query
return _queryMediator.StreamAsync(new StreamProductsBySearchQuery { SearchTerm = term });
}
}
The Query Module utilizes several advanced features that are shared across all LiteBus modules. For detailed explanations, see the dedicated pages:
-
Handler Priority: Control the execution order of pre-handlers and post-handlers using the
[HandlerPriority]
attribute. - Handler Filtering: Selectively execute handlers based on context using tags or predicates.
- Execution Context: Share data between handlers and control the execution flow, for example, to implement caching in a pre-handler.
- Polymorphic Dispatch: Create handlers for base query types that can process derived queries.
- Generic Messages & Handlers: Build reusable, generic queries and handlers.
- Side-Effect Free: Query handlers must never modify system state. They are strictly for data retrieval.
- Return DTOs: Handlers should return Data Transfer Objects (DTOs) or view models, not domain entities, to prevent leaking domain logic and to create optimized data shapes for the consumer.
- Caching: Use post-handlers to cache query results or pre-handlers to return cached data and abort the pipeline.
-
Use Streams for Large Data: Leverage
IStreamQuery
to handle large datasets efficiently without high memory consumption.