| .. | ||
| basic-queries.md | ||
| query-attributes.md | ||
| query-authorization.md | ||
| query-registration.md | ||
| README.md | ||
Queries Overview
Queries represent read operations that retrieve data without modifying state.
What are Queries?
Queries are interrogative requests to fetch data from your system. They never change state and always return results.
Characteristics:
- ✅ Question-based names - GetUser, SearchProducts, ListOrders
- ✅ Read-only - Never modify state
- ✅ Always return data - Must return a result
- ✅ Idempotent - Can call multiple times safely
- ✅ Cacheable - Results can be cached
- ✅ Fast - Should be optimized for performance
Basic Query Example
// Query
public record GetUserQuery
{
public int UserId { get; init; }
}
// DTO (result)
public record UserDto
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
// Handler
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
private readonly IUserRepository _userRepository;
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(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
};
}
}
// Registration
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();
// Endpoints:
// GET /api/query/getUser?userId=1
// POST /api/query/getUser (with JSON body)
HTTP Support
Queries automatically get both GET and POST endpoints:
GET with Query String
curl "http://localhost:5000/api/query/getUser?userId=123"
POST with JSON Body
curl -X POST http://localhost:5000/api/query/getUser \
-H "Content-Type: application/json" \
-d '{"userId": 123}'
Query Documentation
Basic Queries
Simple query patterns:
- Single entity queries
- List queries
- Search queries
- Projection queries
Query Registration
How to register queries:
- Basic registration
- Registration with validators
- Bulk registration
- Organizing registrations
Query Authorization
Securing queries:
- IQueryAuthorizationService
- Row-level security
- Tenant isolation
- Resource ownership
Query Attributes
Controlling query behavior:
- [QueryName] - Custom endpoint names
- [IgnoreQuery] - Internal queries
- [GrpcIgnore] - HTTP-only queries
Common Query Patterns
Pattern 1: Get by ID
public record GetOrderQuery
{
public int OrderId { get; init; }
}
public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderDto>
{
public async Task<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken)
{
var order = await _orders.GetByIdAsync(query.OrderId, cancellationToken);
if (order == null)
throw new KeyNotFoundException($"Order {query.OrderId} not found");
return MapToDto(order);
}
}
Pattern 2: List with Pagination
public record ListUsersQuery
{
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
public class ListUsersQueryHandler : IQueryHandler<ListUsersQuery, PagedResult<UserDto>>
{
public async Task<PagedResult<UserDto>> HandleAsync(ListUsersQuery query, CancellationToken cancellationToken)
{
var totalCount = await _users.CountAsync(cancellationToken);
var users = await _users.GetAllAsync()
.Skip((query.Page - 1) * query.PageSize)
.Take(query.PageSize)
.ToListAsync(cancellationToken);
return new PagedResult<UserDto>
{
Items = users.Select(MapToDto).ToList(),
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
}
}
Pattern 3: Search
public record SearchProductsQuery
{
public string Keyword { get; init; } = string.Empty;
public decimal? MinPrice { get; init; }
public decimal? MaxPrice { get; init; }
}
public class SearchProductsQueryHandler : IQueryHandler<SearchProductsQuery, List<ProductDto>>
{
public async Task<List<ProductDto>> HandleAsync(SearchProductsQuery query, CancellationToken cancellationToken)
{
var products = _products.GetAllAsync();
if (!string.IsNullOrWhiteSpace(query.Keyword))
{
products = products.Where(p =>
p.Name.Contains(query.Keyword) ||
p.Description.Contains(query.Keyword));
}
if (query.MinPrice.HasValue)
products = products.Where(p => p.Price >= query.MinPrice.Value);
if (query.MaxPrice.HasValue)
products = products.Where(p => p.Price <= query.MaxPrice.Value);
var result = await products.ToListAsync(cancellationToken);
return result.Select(MapToDto).ToList();
}
}
Pattern 4: Aggregation
public record GetOrderStatisticsQuery
{
public int CustomerId { get; init; }
}
public record OrderStatistics
{
public int TotalOrders { get; init; }
public decimal TotalSpent { get; init; }
public decimal AverageOrderValue { get; init; }
public DateTime? LastOrderDate { get; init; }
}
public class GetOrderStatisticsQueryHandler : IQueryHandler<GetOrderStatisticsQuery, OrderStatistics>
{
public async Task<OrderStatistics> HandleAsync(GetOrderStatisticsQuery query, CancellationToken cancellationToken)
{
var orders = await _orders
.Where(o => o.CustomerId == query.CustomerId)
.ToListAsync(cancellationToken);
return new OrderStatistics
{
TotalOrders = orders.Count,
TotalSpent = orders.Sum(o => o.TotalAmount),
AverageOrderValue = orders.Any() ? orders.Average(o => o.TotalAmount) : 0,
LastOrderDate = orders.Max(o => (DateTime?)o.CreatedAt)
};
}
}
Best Practices
✅ DO
- Always return DTOs, never domain entities
- Keep queries simple and focused
- Use pagination for large result sets
- Optimize database queries (projections, indexes)
- Handle "not found" cases
- Use async/await consistently
- Accept CancellationToken
❌ DON'T
- Don't modify state in queries
- Don't return IQueryable (always materialize)
- Don't include sensitive data in DTOs
- Don't fetch unnecessary data
- Don't skip pagination for large datasets
- Don't return null (throw KeyNotFoundException instead)
GET vs POST
Use GET When:
- ✅ Simple parameters (IDs, strings)
- ✅ No sensitive data
- ✅ Results can be cached
- ✅ Idempotent
Use POST When:
- ✅ Complex parameters (objects, arrays)
- ✅ Sensitive data
- ✅ Long query strings
- ✅ Need request body
Good news: Both are generated automatically!
What's Next?
- Basic Queries - Common query patterns
- Query Registration - How to register
- Query Authorization - Securing queries
- Query Attributes - Customization