# Your First Query Build your first query handler to retrieve data via HTTP or gRPC. ## What You'll Build In this guide, you'll create a `GetUserQuery` that: - ✅ Accepts a user ID - ✅ Retrieves user data - ✅ Returns a DTO (Data Transfer Object) - ✅ Supports both HTTP GET and POST ## Step 1: Create a DTO DTOs represent the data you return from queries. Create `Models/UserDto.cs`: ```csharp namespace MyApp.Models; 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; } } ``` **Key Points:** - ✅ Use `record` for immutability - ✅ Only include data needed by clients - ✅ Never expose domain entities directly - ✅ Can be different from your database model ## Step 2: Create the Query Queries define what data you're asking for. Create `Queries/GetUserQuery.cs`: ```csharp namespace MyApp.Queries; public record GetUserQuery { public int UserId { get; init; } } ``` **Key Points:** - ✅ Use `record` for immutability - ✅ Name should end with "Query" (convention) - ✅ Contains only the parameters needed to fetch data - ✅ No business logic ## Step 3: Create the Handler Handlers execute the query logic. Create `Queries/GetUserQueryHandler.cs`: ```csharp using Svrnty.CQRS.Abstractions; using MyApp.Models; namespace MyApp.Queries; public class GetUserQueryHandler : IQueryHandler { // In-memory data for demo purposes private static readonly List _users = new() { new User { Id = 1, Name = "Alice Smith", Email = "alice@example.com" }, new User { Id = 2, Name = "Bob Johnson", Email = "bob@example.com" }, }; public Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { var user = _users.FirstOrDefault(u => u.Id == query.UserId); if (user == null) { throw new KeyNotFoundException($"User with ID {query.UserId} not found"); } var dto = new UserDto { Id = user.Id, Name = user.Name, Email = user.Email, CreatedAt = user.CreatedAt }; return Task.FromResult(dto); } } ``` **Handler Interface:** ```csharp IQueryHandler ``` - `TQuery`: Your query type (GetUserQuery) - `TResult`: Return type (UserDto) **Note:** Queries ALWAYS return a result (unlike commands). ## Step 4: Register the Handler In `Program.cs`, register the query handler: ```csharp using Svrnty.CQRS.Abstractions; var builder = WebApplication.CreateBuilder(args); // Register CQRS core services builder.Services.AddSvrntyCQRS(); builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); // ← Add this for queries // Register command (from previous guide) builder.Services.AddCommand(); // Register query builder.Services.AddQuery(); var app = builder.Build(); // Map CQRS endpoints app.UseSvrntyCqrs(); app.Run(); ``` **Registration Syntax:** ```csharp services.AddQuery(); ``` ## Step 5: Test Your Query ### Using HTTP GET Run your application: ```bash dotnet run ``` The query is automatically exposed at: ``` GET /api/query/getUser?userId=1 POST /api/query/getUser ``` Test with curl (GET): ```bash curl "http://localhost:5000/api/query/getUser?userId=1" ``` Expected response: ```json { "id": 1, "name": "Alice Smith", "email": "alice@example.com", "createdAt": "2025-01-15T10:30:00Z" } ``` ### Using HTTP POST You can also POST the query parameters: ```bash curl -X POST http://localhost:5000/api/query/getUser \ -H "Content-Type: application/json" \ -d '{"userId": 1}' ``` Same response as GET. ### Using Swagger Navigate to: ``` http://localhost:5000/swagger ``` You'll see your query listed under "Queries" with both GET and POST endpoints. ## Complete Example with Repository Here's a more realistic example using dependency injection: ### Update the Repository ```csharp // Repositories/IUserRepository.cs namespace MyApp.Repositories; public interface IUserRepository { Task GetByIdAsync(int id, CancellationToken cancellationToken); Task> GetAllAsync(CancellationToken cancellationToken); } // Repositories/InMemoryUserRepository.cs public class InMemoryUserRepository : IUserRepository { private readonly List _users = new() { new User { Id = 1, Name = "Alice Smith", Email = "alice@example.com" }, new User { Id = 2, Name = "Bob Johnson", Email = "bob@example.com" }, }; public Task GetByIdAsync(int id, CancellationToken cancellationToken) { var user = _users.FirstOrDefault(u => u.Id == id); return Task.FromResult(user); } public Task> GetAllAsync(CancellationToken cancellationToken) { return Task.FromResult(_users.ToList()); } } ``` ### Update the Handler ```csharp using Svrnty.CQRS.Abstractions; using MyApp.Models; using MyApp.Repositories; namespace MyApp.Queries; public class GetUserQueryHandler : IQueryHandler { private readonly IUserRepository _userRepository; private readonly ILogger _logger; public GetUserQueryHandler( IUserRepository userRepository, ILogger logger) { _userRepository = userRepository; _logger = logger; } public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { _logger.LogInformation("Fetching user {UserId}", query.UserId); var user = await _userRepository.GetByIdAsync(query.UserId, cancellationToken); if (user == null) { throw new KeyNotFoundException($"User with ID {query.UserId} not found"); } return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email, CreatedAt = user.CreatedAt }; } } ``` ## Query Naming Conventions ### Automatic Endpoint Names Endpoints are generated from the query class name: | Class Name | HTTP Endpoints | |------------|----------------| | `GetUserQuery` | `GET /api/query/getUser?userId=1`
`POST /api/query/getUser` | | `SearchProductsQuery` | `GET /api/query/searchProducts?keyword=...`
`POST /api/query/searchProducts` | | `ListOrdersQuery` | `GET /api/query/listOrders`
`POST /api/query/listOrders` | **Rules:** 1. Strips "Query" suffix 2. Converts to lowerCamelCase 3. Prefixes with `/api/query/` 4. Creates both GET and POST endpoints ### Custom Endpoint Names Use the `[QueryName]` attribute: ```csharp using Svrnty.CQRS.Abstractions; [QueryName("user")] public record GetUserQuery { public int UserId { get; init; } } ``` Endpoints become: ``` GET /api/query/user?userId=1 POST /api/query/user ``` ## Returning Collections Queries can return lists or collections: ```csharp // Query public record ListUsersQuery { public int Page { get; init; } = 1; public int PageSize { get; init; } = 10; } // Handler public class ListUsersQueryHandler : IQueryHandler> { private readonly IUserRepository _userRepository; public ListUsersQueryHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task> HandleAsync(ListUsersQuery query, CancellationToken cancellationToken) { var users = await _userRepository.GetAllAsync(cancellationToken); var dtos = users .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) .Select(u => new UserDto { Id = u.Id, Name = u.Name, Email = u.Email, CreatedAt = u.CreatedAt }) .ToList(); return dtos; } } // Registration builder.Services.AddQuery, ListUsersQueryHandler>(); ``` Test with: ```bash curl "http://localhost:5000/api/query/listUsers?page=1&pageSize=10" ``` ## Returning Complex Types Queries can return nested DTOs: ```csharp // DTOs public record OrderDto { public int OrderId { get; init; } public CustomerDto Customer { get; init; } = null!; public List Items { get; init; } = new(); public decimal TotalAmount { get; init; } } public record CustomerDto { public int Id { get; init; } public string Name { get; init; } = string.Empty; } public record OrderItemDto { public string ProductName { get; init; } = string.Empty; public int Quantity { get; init; } public decimal Price { get; init; } } // Query public record GetOrderQuery { public int OrderId { get; init; } } // Handler public class GetOrderQueryHandler : IQueryHandler { public async Task HandleAsync(GetOrderQuery query, CancellationToken cancellationToken) { // Fetch and map your data return new OrderDto { OrderId = query.OrderId, Customer = new CustomerDto { Id = 1, Name = "Alice" }, Items = new List { new() { ProductName = "Widget", Quantity = 2, Price = 10.00m } }, TotalAmount = 20.00m }; } } ``` ## Error Handling ### Not Found Throw `KeyNotFoundException` for missing entities: ```csharp public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(query.UserId, cancellationToken); if (user == null) { throw new KeyNotFoundException($"User with ID {query.UserId} not found"); } return MapToDto(user); } ``` HTTP response: ``` 404 Not Found ``` ### Validation Errors Throw `ArgumentException` for invalid input: ```csharp public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { if (query.UserId <= 0) { throw new ArgumentException("UserId must be greater than 0", nameof(query.UserId)); } // ... fetch user } ``` HTTP response: ``` 400 Bad Request ``` ## Best Practices ### ✅ DO - **Return DTOs** - Never return domain entities - **Keep queries simple** - One query = one data need - **Use async/await** - Even for in-memory data - **Include only needed data** - Don't over-fetch - **Support GET and POST** - Both are generated automatically - **Use meaningful names** - GetUser, SearchOrders, ListProducts - **Handle not found** - Throw KeyNotFoundException ### ❌ DON'T - **Don't modify state** - Queries should be read-only - **Don't use queries for commands** - Use commands to change state - **Don't return IQueryable** - Always materialize results - **Don't include sensitive data** - Filter out passwords, tokens, etc. - **Don't ignore pagination** - For large result sets - **Don't fetch unnecessary data** - Use projections ## GET vs POST for Queries ### When to Use GET - ✅ Simple parameters (IDs, strings, numbers) - ✅ No sensitive data in parameters - ✅ Results can be cached - ✅ Idempotent operations Example: ``` GET /api/query/getUser?userId=123 ``` ### When to Use POST - ✅ Complex parameters (objects, arrays) - ✅ Sensitive data in parameters - ✅ Long query strings - ✅ Need request body Example: ``` POST /api/query/searchOrders { "filters": { "status": "completed", "customerId": 123 }, "sorts": [{ "field": "orderDate", "direction": "desc" }], "page": 1, "pageSize": 20 } ``` **Good news:** Svrnty.CQRS creates **both** endpoints automatically! ## Troubleshooting ### Query Returns 404 **Problem:** Endpoint exists but always returns 404 **Solutions:** 1. Check your error handling - are you throwing KeyNotFoundException? 2. Verify data actually exists 3. Ensure query parameters are passed correctly ### Query Parameters Not Binding **Problem:** Parameters are null or default values **Solutions:** 1. Check property names match query string (case-insensitive) 2. For GET, use query string: `?userId=1` 3. For POST, use JSON body: `{"userId": 1}` ### Query Too Slow **Problem:** Query takes too long to execute **Solutions:** 1. Add database indexes 2. Use projections (select only needed columns) 3. Implement pagination 4. Consider caching 5. Use dynamic queries for flexible filtering ## What's Next? Now that you can query data, let's add validation to ensure data quality! **Continue to [Adding Validation](05-adding-validation.md) →** ## See Also - [Queries Overview](../core-features/queries/README.md) - Deep dive into queries - [Dynamic Queries](../core-features/dynamic-queries/README.md) - Advanced querying with filters - [Query Authorization](../core-features/queries/query-authorization.md) - Secure your queries - [Best Practices: Query Design](../best-practices/query-design.md) - Query optimization patterns