Queries - PogovorovDaniil/Requestum GitHub Wiki
Queries are the foundation of data retrieval operations in Requestum. This guide covers everything you need to know about working with queries.
A query is a request that retrieves data from the system. Queries:
- Represent questions or data requests
- Do not modify system state (read operations)
- Return typed responses
- Have exactly one handler
- Use descriptive naming (questions)
// Query
public record GetUserByIdQuery(int UserId) : IQuery<UserDto>;
// Response
public record UserDto
{
public int Id { get; init; }
public required string Name { get; init; }
public required string Email { get; init; }
}public record SearchUsersQuery : IQuery<List<UserDto>>
{
public required string SearchTerm { get; init; }
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
public bool IncludeInactive { get; init; } = false;
}public record GetAllActiveUsersQuery : IQuery<List<UserDto>>;public record GetOrdersQuery : IQuery<PagedResult<OrderDto>>
{
public int? CustomerId { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public OrderStatus? Status { get; init; }
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 20;
public string? SortBy { get; init; }
public bool SortDescending { get; init; } = false;
}
public record PagedResult<T>
{
public List<T> Items { get; init; } = new();
public int TotalCount { get; init; }
public int PageNumber { get; init; }
public int PageSize { get; init; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}Use when the operation is purely synchronous (in-memory, cache):
public class CalculateTotalQueryHandler : IQueryHandler<CalculateTotalQuery, decimal>
{
public decimal Handle(CalculateTotalQuery query)
{
// Pure calculation, no I/O
return query.Items.Sum(item => item.Price * item.Quantity);
}
}Use when the operation involves I/O (database, network, files):
public class GetUserByIdQueryHandler : IAsyncQueryHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _repository;
private readonly ILogger<GetUserByIdQueryHandler> _logger;
public GetUserByIdQueryHandler(
IUserRepository repository,
ILogger<GetUserByIdQueryHandler> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<UserDto> HandleAsync(
GetUserByIdQuery query,
CancellationToken ct = default)
{
_logger.LogInformation("Fetching user with ID: {UserId}", query.UserId);
var user = await _repository.GetByIdAsync(query.UserId, ct);
if (user == null)
{
throw new NotFoundException($"User with ID {query.UserId} not found");
}
return new UserDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
}
}var query = new GetUserByIdQuery(UserId: 123);
// Synchronous execution
var user = requestum.Handle<GetUserByIdQuery, UserDto>(query);
// Asynchronous execution
var userAsync = await requestum.HandleAsync<GetUserByIdQuery, UserDto>(query);
// With cancellation token
var userWithToken = await requestum.HandleAsync<GetUserByIdQuery, UserDto>(query, cancellationToken);For queries with parameterless constructors:
// Synchronous
var users = requestum.Handle<GetAllUsersQuery, List<UserDto>>();
// Asynchronous
var usersAsync = await requestum.HandleAsync<GetAllUsersQuery, List<UserDto>>();[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IRequestum _requestum;
public UsersController(IRequestum requestum) => _requestum = requestum;
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(int id, CancellationToken ct)
{
var query = new GetUserByIdQuery(id);
var user = await _requestum.HandleAsync<GetUserByIdQuery, UserDto>(query, ct);
return Ok(user);
}
[HttpGet]
public async Task<ActionResult<PagedResult<UserDto>>> SearchUsers(
[FromQuery] string? searchTerm,
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
CancellationToken ct = default)
{
var query = new SearchUsersQuery
{
SearchTerm = searchTerm ?? "",
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _requestum.HandleAsync<SearchUsersQuery, PagedResult<UserDto>>(query, ct);
return Ok(result);
}
}public record GetUserByEmailQuery(string Email) : IQuery<UserDto?>;
public class GetUserByEmailQueryHandler : IAsyncQueryHandler<GetUserByEmailQuery, UserDto?>
{
private readonly IUserRepository _repository;
public async Task<UserDto?> HandleAsync(GetUserByEmailQuery query, CancellationToken ct = default)
{
var user = await _repository.FindByEmailAsync(query.Email, ct);
if (user == null)
return null;
return new UserDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
}
}public record GetUsersPagedQuery : IQuery<PagedResult<UserDto>>
{
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
public class GetUsersPagedQueryHandler : IAsyncQueryHandler<GetUsersPagedQuery, PagedResult<UserDto>>
{
private readonly IUserRepository _repository;
public async Task<PagedResult<UserDto>> HandleAsync(
GetUsersPagedQuery query,
CancellationToken ct = default)
{
var totalCount = await _repository.CountAsync(ct);
var users = await _repository.GetPagedAsync(
query.PageNumber,
query.PageSize,
ct);
var userDtos = users.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
}).ToList();
return new PagedResult<UserDto>
{
Items = userDtos,
TotalCount = totalCount,
PageNumber = query.PageNumber,
PageSize = query.PageSize
};
}
}public record SearchProductsQuery : IQuery<List<ProductDto>>
{
public string? SearchTerm { get; init; }
public decimal? MinPrice { get; init; }
public decimal? MaxPrice { get; init; }
public int? CategoryId { get; init; }
public bool InStockOnly { get; init; }
}
public class SearchProductsQueryHandler : IAsyncQueryHandler<SearchProductsQuery, List<ProductDto>>
{
private readonly IProductRepository _repository;
public async Task<List<ProductDto>> HandleAsync(
SearchProductsQuery query,
CancellationToken ct = default)
{
var products = await _repository.SearchAsync(
searchTerm: query.SearchTerm,
minPrice: query.MinPrice,
maxPrice: query.MaxPrice,
categoryId: query.CategoryId,
inStockOnly: query.InStockOnly,
ct);
return products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Stock = p.Stock
}).ToList();
}
}public record GetOrderStatisticsQuery : IQuery<OrderStatisticsDto>
{
public DateTime FromDate { get; init; }
public DateTime ToDate { get; init; }
}
public record OrderStatisticsDto
{
public int TotalOrders { get; init; }
public decimal TotalRevenue { get; init; }
public decimal AverageOrderValue { get; init; }
public int UniqueCustomers { get; init; }
}
public class GetOrderStatisticsQueryHandler : IAsyncQueryHandler<GetOrderStatisticsQuery, OrderStatisticsDto>
{
private readonly IOrderRepository _repository;
public async Task<OrderStatisticsDto> HandleAsync(
GetOrderStatisticsQuery query,
CancellationToken ct = default)
{
var orders = await _repository.GetByDateRangeAsync(
query.FromDate,
query.ToDate,
ct);
return new OrderStatisticsDto
{
TotalOrders = orders.Count,
TotalRevenue = orders.Sum(o => o.Total),
AverageOrderValue = orders.Any() ? orders.Average(o => o.Total) : 0,
UniqueCustomers = orders.Select(o => o.CustomerId).Distinct().Count()
};
}
}public record GetOrderDetailsQuery(int OrderId) : IQuery<OrderDetailsDto>;
public record OrderDetailsDto
{
public int OrderId { get; init; }
public CustomerDto Customer { get; init; } = null!;
public List<OrderItemDto> Items { get; init; } = new();
public decimal Total { get; init; }
public OrderStatus Status { get; init; }
}
public class GetOrderDetailsQueryHandler : IAsyncQueryHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IProductRepository _productRepository;
public GetOrderDetailsQueryHandler(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IProductRepository productRepository)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_productRepository = productRepository;
}
public async Task<OrderDetailsDto> HandleAsync(
GetOrderDetailsQuery query,
CancellationToken ct = default)
{
var order = await _orderRepository.GetByIdAsync(query.OrderId, ct)
?? throw new NotFoundException($"Order {query.OrderId} not found");
var customer = await _customerRepository.GetByIdAsync(order.CustomerId, ct)
?? throw new NotFoundException($"Customer {order.CustomerId} not found");
var orderItems = new List<OrderItemDto>();
foreach (var item in order.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId, ct);
orderItems.Add(new OrderItemDto
{
ProductId = item.ProductId,
ProductName = product?.Name ?? "Unknown",
Quantity = item.Quantity,
Price = item.Price
});
}
return new OrderDetailsDto
{
OrderId = order.Id,
Customer = new CustomerDto
{
Id = customer.Id,
Name = customer.Name,
Email = customer.Email
},
Items = orderItems,
Total = order.Total,
Status = order.Status
};
}
}public record GetProductCatalogQuery : IQuery<List<ProductDto>>;
public class GetProductCatalogQueryHandler : IAsyncQueryHandler<GetProductCatalogQuery, List<ProductDto>>
{
private readonly IProductRepository _repository;
private readonly IMemoryCache _cache;
private readonly ILogger<GetProductCatalogQueryHandler> _logger;
private const string CacheKey = "ProductCatalog";
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
public async Task<List<ProductDto>> HandleAsync(
GetProductCatalogQuery query,
CancellationToken ct = default)
{
// Try get from cache
if (_cache.TryGetValue<List<ProductDto>>(CacheKey, out var cachedProducts))
{
_logger.LogInformation("Returning cached product catalog");
return cachedProducts!;
}
// Cache miss - fetch from database
_logger.LogInformation("Cache miss - fetching product catalog from database");
var products = await _repository.GetAllAsync(ct);
var productDtos = products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Stock = p.Stock
}).ToList();
// Store in cache
_cache.Set(CacheKey, productDtos, CacheDuration);
return productDtos;
}
}public class GetUserByIdQueryHandler : IAsyncQueryHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _repository;
public async Task<UserDto> HandleAsync(GetUserByIdQuery query, CancellationToken ct = default)
{
var user = await _repository.GetByIdAsync(query.UserId, ct);
if (user == null)
{
throw new NotFoundException($"User with ID {query.UserId} not found");
}
return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email };
}
}public record GetUserByEmailQuery(string Email) : IQuery<UserDto?>;
public class GetUserByEmailQueryHandler : IAsyncQueryHandler<GetUserByEmailQuery, UserDto?>
{
private readonly IUserRepository _repository;
public async Task<UserDto?> HandleAsync(GetUserByEmailQuery query, CancellationToken ct = default)
{
var user = await _repository.FindByEmailAsync(query.Email, ct);
// Return null if not found
if (user == null)
return null;
return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email };
}
}public record Result<T>
{
public bool IsSuccess { get; init; }
public T? Data { get; init; }
public string? ErrorMessage { get; init; }
public static Result<T> Success(T data) => new() { IsSuccess = true, Data = data };
public static Result<T> Failure(string error) => new() { IsSuccess = false, ErrorMessage = error };
}
public record GetUserByIdQuery(int UserId) : IQuery<Result<UserDto>>;
public class GetUserByIdQueryHandler : IAsyncQueryHandler<GetUserByIdQuery, Result<UserDto>>
{
private readonly IUserRepository _repository;
public async Task<Result<UserDto>> HandleAsync(GetUserByIdQuery query, CancellationToken ct = default)
{
try
{
var user = await _repository.GetByIdAsync(query.UserId, ct);
if (user == null)
return Result<UserDto>.Failure($"User with ID {query.UserId} not found");
var dto = new UserDto { Id = user.Id, Name = user.Name, Email = user.Email };
return Result<UserDto>.Success(dto);
}
catch (Exception ex)
{
return Result<UserDto>.Failure($"Error fetching user: {ex.Message}");
}
}
}// ✅ Good - clear what it does
public record GetUserByIdQuery(int UserId) : IQuery<UserDto>;
public record SearchProductsByCategoryQuery(int CategoryId) : IQuery<List<ProductDto>>;
public record CalculateOrderTotalQuery(int OrderId) : IQuery<decimal>;
// ❌ Bad - unclear intent
public record UserQuery(int Id) : IQuery<UserDto>;
public record ProductQuery(int CategoryId) : IQuery<List<ProductDto>>;
public record TotalQuery(int Id) : IQuery<decimal>;// ✅ Good - read-only query
public class GetUserByIdQueryHandler : IAsyncQueryHandler<GetUserByIdQuery, UserDto>
{
public async Task<UserDto> HandleAsync(GetUserByIdQuery query, CancellationToken ct = default)
{
var user = await _repository.GetByIdAsync(query.UserId, ct);
return MapToDto(user);
}
}
// ❌ Bad - modifying state in query
public class GetUserByIdQueryHandler : IAsyncQueryHandler<GetUserByIdQuery, UserDto>
{
public async Task<UserDto> HandleAsync(GetUserByIdQuery query, CancellationToken ct = default)
{
var user = await _repository.GetByIdAsync(query.UserId, ct);
// Don't do this in a query!
user.LastAccessedAt = DateTime.UtcNow;
await _repository.UpdateAsync(user, ct);
return MapToDto(user);
}
}// ✅ Good - DTO response
public record UserDto
{
public int Id { get; init; }
public required string Name { get; init; }
public required string Email { get; init; }
}
public record GetUserQuery(int UserId) : IQuery<UserDto>;
// ❌ Bad - returning domain entity
public record GetUserQuery(int UserId) : IQuery<User>; // User is domain entity// ✅ Good - focused queries
public record GetUserByIdQuery(int UserId) : IQuery<UserDto>;
public record GetUserOrdersQuery(int UserId) : IQuery<List<OrderDto>>;
public record GetUserAddressesQuery(int UserId) : IQuery<List<AddressDto>>;
// ❌ Bad - one query doing too much
public record GetCompleteUserDataQuery(int UserId) : IQuery<CompleteUserDto>;
// Where CompleteUserDto includes everything: profile, orders, addresses, payments, etc.// ✅ Good - paginated
public record GetOrdersQuery : IQuery<PagedResult<OrderDto>>
{
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 20;
}
// ❌ Bad - fetching all data
public record GetAllOrdersQuery : IQuery<List<OrderDto>>; // Could return millions of records// ✅ Good - immutable
public record GetUserByIdQuery(int UserId) : IQuery<UserDto>;
// ✅ Good - immutable with init
public record SearchUsersQuery : IQuery<List<UserDto>>
{
public required string SearchTerm { get; init; }
public int PageNumber { get; init; } = 1;
}
// ❌ Bad - mutable
public class SearchUsersQuery : IQuery<List<UserDto>>
{
public string SearchTerm { get; set; }
public int PageNumber { get; set; }
}- Events - Learn about event-driven architecture
- Middleware Pipeline - Add cross-cutting concerns
← Commands | Home | Events →