295 lines
7.4 KiB
Markdown
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)
|