dotnet-cqrs/docs/core-features/queries/README.md

295 lines
7.4 KiB
Markdown

# 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
```csharp
// 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
```bash
curl "http://localhost:5000/api/query/getUser?userId=123"
```
### POST with JSON Body
```bash
curl -X POST http://localhost:5000/api/query/getUser \
-H "Content-Type: application/json" \
-d '{"userId": 123}'
```
## Query Documentation
### [Basic Queries](basic-queries.md)
Simple query patterns:
- Single entity queries
- List queries
- Search queries
- Projection queries
### [Query Registration](query-registration.md)
How to register queries:
- Basic registration
- Registration with validators
- Bulk registration
- Organizing registrations
### [Query Authorization](query-authorization.md)
Securing queries:
- IQueryAuthorizationService
- Row-level security
- Tenant isolation
- Resource ownership
### [Query Attributes](query-attributes.md)
Controlling query behavior:
- [QueryName] - Custom endpoint names
- [IgnoreQuery] - Internal queries
- [GrpcIgnore] - HTTP-only queries
## Common Query Patterns
### Pattern 1: Get by ID
```csharp
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
```csharp
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
```csharp
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
```csharp
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](basic-queries.md)** - Common query patterns
- **[Query Registration](query-registration.md)** - How to register
- **[Query Authorization](query-authorization.md)** - Securing queries
- **[Query Attributes](query-attributes.md)** - Customization
## See Also
- [Commands Overview](../commands/README.md)
- [Dynamic Queries](../dynamic-queries/README.md)
- [Getting Started: Your First Query](../../getting-started/04-first-query.md)
- [Best Practices: Query Design](../../best-practices/query-design.md)