# Your First Command Build your first command handler step-by-step and expose it via HTTP or gRPC. ## What You'll Build In this guide, you'll create a `CreateUserCommand` that: - ✅ Accepts user data (name, email) - ✅ Creates a new user - ✅ Returns the generated user ID - ✅ Is automatically exposed as an HTTP endpoint ## Step 1: Create the Command Commands are simple POCOs (Plain Old CLR Objects). Create a new file `Commands/CreateUserCommand.cs`: ```csharp namespace MyApp.Commands; public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } ``` **Key Points:** - ✅ Use `record` for immutability (recommended) - ✅ Properties should be `init`-only - ✅ No base class or interface required - ✅ Name should end with "Command" (convention) ## Step 2: Create the Handler Handlers contain your business logic. Create `Commands/CreateUserCommandHandler.cs`: ```csharp using Svrnty.CQRS.Abstractions; namespace MyApp.Commands; public class CreateUserCommandHandler : ICommandHandler { public Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // TODO: Add your business logic here // For now, return a random ID var userId = new Random().Next(1, 1000); Console.WriteLine($"Creating user: {command.Name} ({command.Email})"); return Task.FromResult(userId); } } ``` **Handler Interface:** ```csharp ICommandHandler ``` - `TCommand`: Your command type (CreateUserCommand) - `TResult`: Return type (int for user ID) For commands without a return value, use: ```csharp ICommandHandler ``` ## Step 3: Register the Handler In your `Program.cs`, register the command handler: ```csharp using Svrnty.CQRS.Abstractions; var builder = WebApplication.CreateBuilder(args); // Register CQRS core services builder.Services.AddSvrntyCQRS(); builder.Services.AddDefaultCommandDiscovery(); // Register your command handler builder.Services.AddCommand(); var app = builder.Build(); // Map CQRS endpoints app.UseSvrntyCqrs(); app.Run(); ``` **Registration Syntax:** ```csharp // Command with result services.AddCommand(); // Command without result services.AddCommand(); ``` ## Step 4: Test Your Command ### Using HTTP Run your application: ```bash dotnet run ``` The command is automatically exposed at: ``` POST /api/command/createUser ``` Test with curl: ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{ "name": "Alice Smith", "email": "alice@example.com" }' ``` Expected response: ```json 456 ``` (The generated user ID) ### Using Swagger If you have Swagger enabled, navigate to: ``` http://localhost:5000/swagger ``` You'll see your command listed under "Commands": ``` POST /api/command/createUser ``` Click "Try it out", fill in the request body, and execute. ## Complete Example Here's a more realistic example with actual data persistence: ### Create a User Model ```csharp // Models/User.cs namespace MyApp.Models; public class User { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } ``` ### Create a Repository ```csharp // Repositories/IUserRepository.cs namespace MyApp.Repositories; public interface IUserRepository { Task AddAsync(User user, CancellationToken cancellationToken); } // Repositories/InMemoryUserRepository.cs public class InMemoryUserRepository : IUserRepository { private readonly List _users = new(); private int _nextId = 1; public Task AddAsync(User user, CancellationToken cancellationToken) { user.Id = _nextId++; user.CreatedAt = DateTime.UtcNow; _users.Add(user); return Task.FromResult(user.Id); } } ``` ### Update the Handler ```csharp using Svrnty.CQRS.Abstractions; using MyApp.Models; using MyApp.Repositories; namespace MyApp.Commands; public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { var user = new User { Name = command.Name, Email = command.Email }; var userId = await _userRepository.AddAsync(user, cancellationToken); Console.WriteLine($"Created user {userId}: {command.Name}"); return userId; } } ``` ### Update Program.cs ```csharp using MyApp.Repositories; using Svrnty.CQRS.Abstractions; var builder = WebApplication.CreateBuilder(args); // Register repository builder.Services.AddSingleton(); // Register CQRS builder.Services.AddSvrntyCQRS(); builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddCommand(); var app = builder.Build(); app.UseSvrntyCqrs(); app.Run(); ``` ## Command Naming Conventions ### Automatic Endpoint Names By default, endpoints are generated from the command class name: | Class Name | HTTP Endpoint | |------------|---------------| | `CreateUserCommand` | `POST /api/command/createUser` | | `UpdateProfileCommand` | `POST /api/command/updateProfile` | | `DeleteOrderCommand` | `POST /api/command/deleteOrder` | **Rules:** 1. Strips "Command" suffix 2. Converts to lowerCamelCase 3. Prefixes with `/api/command/` ### Custom Endpoint Names Use the `[CommandName]` attribute to customize: ```csharp using Svrnty.CQRS.Abstractions; [CommandName("register")] public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } ``` Endpoint becomes: ``` POST /api/command/register ``` ## Commands Without Results Some commands don't need to return a value: ```csharp // Command public record DeleteUserCommand { public int UserId { get; init; } } // Handler public class DeleteUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public DeleteUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { await _userRepository.DeleteAsync(command.UserId, cancellationToken); // No return value } } // Registration builder.Services.AddCommand(); ``` HTTP response: ``` 204 No Content ``` ## Dependency Injection Handlers support full dependency injection: ```csharp public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly IEmailService _emailService; private readonly ILogger _logger; public CreateUserCommandHandler( IUserRepository userRepository, IEmailService emailService, ILogger logger) { _userRepository = userRepository; _emailService = emailService; _logger = logger; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { _logger.LogInformation("Creating user: {Email}", command.Email); var user = new User { Name = command.Name, Email = command.Email }; var userId = await _userRepository.AddAsync(user, cancellationToken); await _emailService.SendWelcomeEmailAsync(user.Email, cancellationToken); return userId; } } ``` All dependencies are resolved from the DI container. ## Best Practices ### ✅ DO - **Use records** - Immutable data structures - **Name clearly** - Use imperative verbs (Create, Update, Delete) - **Keep commands simple** - Just data, no logic - **Validate in handlers** - Or use validators (next guide) - **Return meaningful results** - IDs, confirmation data - **Use async/await** - Even for synchronous operations - **Accept CancellationToken** - Enable request cancellation ### ❌ DON'T - **Don't put logic in commands** - Commands are just data - **Don't return domain entities** - Use DTOs or primitives - **Don't ignore CancellationToken** - Always pass it through - **Don't use constructors** - Use init-only properties - **Don't make properties mutable** - Use `init` instead of `set` ## Troubleshooting ### Endpoint Not Found **Problem:** `404 Not Found` when calling `/api/command/createUser` **Solutions:** 1. Ensure you called `app.UseSvrntyCqrs()` in Program.cs 2. Verify the command is registered with `AddCommand<>()` 3. Check the command name matches the endpoint (or use `[CommandName]`) ### Handler Not Executing **Problem:** Endpoint exists but handler doesn't run **Solutions:** 1. Verify handler is registered in DI 2. Check for exceptions in handler constructor (DI failure) 3. Ensure handler implements correct interface ### JSON Deserialization Fails **Problem:** `400 Bad Request` with serialization error **Solutions:** 1. Check property names match JSON (case-insensitive by default) 2. Ensure all properties have public getters 3. Use `init` instead of private setters ## What's Next? Now that you can create commands, let's learn how to query data! **Continue to [Your First Query](04-first-query.md) →** ## See Also - [Commands Overview](../core-features/commands/README.md) - Deep dive into commands - [Command Registration](../core-features/commands/command-registration.md) - Advanced registration patterns - [Best Practices: Command Design](../best-practices/command-design.md) - Command design patterns - [Validation](05-adding-validation.md) - Add FluentValidation to your commands