# Basic Queries Common query patterns for retrieving data. ## Overview Basic queries are the most common read operations in CQRS applications. This guide covers standard patterns for fetching entities, lists, and aggregated data. ## Single Entity Queries ### Get by ID ```csharp public record GetUserQuery { public int UserId { get; init; } } public record UserDto { public int Id { get; init; } public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; public DateTime CreatedAt { get; init; } } public class GetUserQueryHandler : IQueryHandler { private readonly ApplicationDbContext _context; public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { var user = await _context.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Id == query.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {query.UserId} not found"); return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email, CreatedAt = user.CreatedAt }; } } ``` ### Get by Unique Field ```csharp public record GetUserByEmailQuery { public string Email { get; init; } = string.Empty; } public class GetUserByEmailQueryHandler : IQueryHandler { public async Task HandleAsync(GetUserByEmailQuery query, CancellationToken cancellationToken) { var user = await _context.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Email == query.Email, cancellationToken); if (user == null) throw new KeyNotFoundException($"User with email {query.Email} not found"); return MapToDto(user); } } ``` ## List Queries ### Simple List ```csharp public record ListUsersQuery { } public class ListUsersQueryHandler : IQueryHandler> { public async Task> HandleAsync(ListUsersQuery query, CancellationToken cancellationToken) { var users = await _context.Users .AsNoTracking() .OrderBy(u => u.Name) .ToListAsync(cancellationToken); return users.Select(u => new UserDto { Id = u.Id, Name = u.Name, Email = u.Email, CreatedAt = u.CreatedAt }).ToList(); } } ``` ### Paginated List ```csharp public record ListUsersQuery { public int Page { get; init; } = 1; public int PageSize { get; init; } = 10; } public record PagedResult { public List Items { get; init; } = new(); public int TotalCount { get; init; } public int Page { get; init; } public int PageSize { get; init; } public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize); } public class ListUsersQueryHandler : IQueryHandler> { public async Task> HandleAsync(ListUsersQuery query, CancellationToken cancellationToken) { var totalCount = await _context.Users.CountAsync(cancellationToken); var users = await _context.Users .AsNoTracking() .OrderBy(u => u.Name) .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) .ToListAsync(cancellationToken); return new PagedResult { Items = users.Select(MapToDto).ToList(), TotalCount = totalCount, Page = query.Page, PageSize = query.PageSize }; } } ``` ### Filtered List ```csharp public record ListActiveUsersQuery { public bool? IsActive { get; init; } } public class ListActiveUsersQueryHandler : IQueryHandler> { public async Task> HandleAsync(ListActiveUsersQuery query, CancellationToken cancellationToken) { var usersQuery = _context.Users.AsNoTracking(); if (query.IsActive.HasValue) { usersQuery = usersQuery.Where(u => u.IsActive == query.IsActive.Value); } var users = await usersQuery .OrderBy(u => u.Name) .ToListAsync(cancellationToken); return users.Select(MapToDto).ToList(); } } ``` ## Search Queries ### Text Search ```csharp public record SearchUsersQuery { public string Keyword { get; init; } = string.Empty; public int Page { get; init; } = 1; public int PageSize { get; init; } = 10; } public class SearchUsersQueryHandler : IQueryHandler> { public async Task> HandleAsync(SearchUsersQuery query, CancellationToken cancellationToken) { var usersQuery = _context.Users.AsNoTracking(); if (!string.IsNullOrWhiteSpace(query.Keyword)) { var keyword = query.Keyword.ToLower(); usersQuery = usersQuery.Where(u => u.Name.ToLower().Contains(keyword) || u.Email.ToLower().Contains(keyword)); } var totalCount = await usersQuery.CountAsync(cancellationToken); var users = await usersQuery .OrderBy(u => u.Name) .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) .ToListAsync(cancellationToken); return new PagedResult { Items = users.Select(MapToDto).ToList(), TotalCount = totalCount, Page = query.Page, PageSize = query.PageSize }; } } ``` ### Multi-Criteria Search ```csharp public record SearchProductsQuery { public string? Keyword { get; init; } public string? Category { get; init; } public decimal? MinPrice { get; init; } public decimal? MaxPrice { get; init; } public bool? InStock { get; init; } } public class SearchProductsQueryHandler : IQueryHandler> { public async Task> HandleAsync(SearchProductsQuery query, CancellationToken cancellationToken) { var productsQuery = _context.Products.AsNoTracking(); if (!string.IsNullOrWhiteSpace(query.Keyword)) { var keyword = query.Keyword.ToLower(); productsQuery = productsQuery.Where(p => p.Name.ToLower().Contains(keyword) || p.Description.ToLower().Contains(keyword)); } if (!string.IsNullOrWhiteSpace(query.Category)) { productsQuery = productsQuery.Where(p => p.Category == query.Category); } if (query.MinPrice.HasValue) { productsQuery = productsQuery.Where(p => p.Price >= query.MinPrice.Value); } if (query.MaxPrice.HasValue) { productsQuery = productsQuery.Where(p => p.Price <= query.MaxPrice.Value); } if (query.InStock.HasValue) { productsQuery = productsQuery.Where(p => p.Stock > 0); } var products = await productsQuery .OrderBy(p => p.Name) .ToListAsync(cancellationToken); return products.Select(MapToDto).ToList(); } } ``` ## Aggregation Queries ### Count ```csharp public record GetUserCountQuery { } public class GetUserCountQueryHandler : IQueryHandler { public async Task HandleAsync(GetUserCountQuery query, CancellationToken cancellationToken) { return await _context.Users.CountAsync(cancellationToken); } } ``` ### Sum and Average ```csharp public record GetOrderStatisticsQuery { public int CustomerId { get; init; } } public record OrderStatistics { public int TotalOrders { get; init; } public decimal TotalAmount { get; init; } public decimal AverageOrderValue { get; init; } } public class GetOrderStatisticsQueryHandler : IQueryHandler { public async Task HandleAsync(GetOrderStatisticsQuery query, CancellationToken cancellationToken) { var statistics = await _context.Orders .Where(o => o.CustomerId == query.CustomerId) .GroupBy(o => o.CustomerId) .Select(g => new OrderStatistics { TotalOrders = g.Count(), TotalAmount = g.Sum(o => o.TotalAmount), AverageOrderValue = g.Average(o => o.TotalAmount) }) .FirstOrDefaultAsync(cancellationToken); return statistics ?? new OrderStatistics(); } } ``` ## Complex Queries ### Nested Data ```csharp public record GetOrderWithDetailsQuery { public int OrderId { get; init; } } public record OrderDto { public int Id { get; init; } public CustomerDto Customer { get; init; } = null!; public List Items { get; init; } = new(); public decimal TotalAmount { get; init; } } public class GetOrderWithDetailsQueryHandler : IQueryHandler { public async Task HandleAsync(GetOrderWithDetailsQuery query, CancellationToken cancellationToken) { var order = await _context.Orders .AsNoTracking() .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(i => i.Product) .FirstOrDefaultAsync(o => o.Id == query.OrderId, cancellationToken); if (order == null) throw new KeyNotFoundException($"Order {query.OrderId} not found"); return new OrderDto { Id = order.Id, Customer = new CustomerDto { Id = order.Customer.Id, Name = order.Customer.Name }, Items = order.Items.Select(i => new OrderItemDto { ProductName = i.Product.Name, Quantity = i.Quantity, Price = i.Price }).ToList(), TotalAmount = order.TotalAmount }; } } ``` ## Performance Optimization ### Use AsNoTracking ```csharp // ✅ Good - AsNoTracking for read-only queries var users = await _context.Users .AsNoTracking() .ToListAsync(cancellationToken); // ❌ Bad - Change tracking overhead var users = await _context.Users .ToListAsync(cancellationToken); ``` ### Use Projections ```csharp // ✅ Good - Select only needed columns var users = await _context.Users .AsNoTracking() .Select(u => new UserDto { Id = u.Id, Name = u.Name, Email = u.Email }) .ToListAsync(cancellationToken); // ❌ Bad - Fetch entire entity then map var users = await _context.Users .AsNoTracking() .ToListAsync(cancellationToken); var dtos = users.Select(MapToDto).ToList(); ``` ### Avoid N+1 Queries ```csharp // ✅ Good - Use Include for related data var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ToListAsync(cancellationToken); // ❌ Bad - N+1 query problem var orders = await _context.Orders.ToListAsync(cancellationToken); foreach (var order in orders) { order.Customer = await _context.Customers.FindAsync(order.CustomerId); } ``` ## Best Practices ### ✅ DO - Use AsNoTracking for read-only queries - Use projections (Select) to fetch only needed data - Always materialize queries (ToListAsync, FirstOrDefaultAsync) - Throw KeyNotFoundException for missing entities - Use pagination for large result sets - Optimize with indexes - Accept CancellationToken ### ❌ DON'T - Don't return IQueryable - Don't return domain entities - Don't modify state - Don't fetch unnecessary data - Don't skip pagination - Don't ignore performance ## See Also - [Query Registration](query-registration.md) - [Query Authorization](query-authorization.md) - [Dynamic Queries](../dynamic-queries/README.md) - [Best Practices: Query Design](../../best-practices/query-design.md)