# Basic Commands Commands that perform actions without returning data (void commands). ## Overview Basic commands execute operations and don't return a result. They use the `ICommandHandler` interface (without a result type parameter). **Use cases:** - Delete operations - Update operations - Toggle/flag operations - Cleanup operations - Fire-and-forget operations ## Interface ```csharp public interface ICommandHandler { Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); } ``` ## Example: Delete User ```csharp // Command public record DeleteUserCommand { public int UserId { get; init; } } // Handler public class DeleteUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly ILogger _logger; public DeleteUserCommandHandler( IUserRepository userRepository, ILogger logger) { _userRepository = userRepository; _logger = logger; } public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { _logger.LogInformation("Deleting user: {UserId}", command.UserId); var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); await _userRepository.DeleteAsync(user, cancellationToken); _logger.LogInformation("User deleted: {UserId}", command.UserId); } } // Registration builder.Services.AddCommand(); ``` ## HTTP Behavior ### Request ```bash curl -X POST http://localhost:5000/api/command/deleteUser \ -H "Content-Type: application/json" \ -d '{"userId": 123}' ``` ### Response **Success:** ``` HTTP/1.1 204 No Content ``` **Error (Not Found):** ``` HTTP/1.1 404 Not Found Content-Type: application/json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", "title": "Not Found", "status": 404, "detail": "User 123 not found" } ``` ## Common Patterns ### Pattern 1: Update Operation ```csharp public record UpdateUserEmailCommand { public int UserId { get; init; } public string NewEmail { get; init; } = string.Empty; } public class UpdateUserEmailCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public async Task HandleAsync(UpdateUserEmailCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); user.Email = command.NewEmail; user.EmailVerified = false; // Reset verification await _userRepository.UpdateAsync(user, cancellationToken); } } ``` ### Pattern 2: Toggle Operation ```csharp public record ToggleUserActiveCommand { public int UserId { get; init; } } public class ToggleUserActiveCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public async Task HandleAsync(ToggleUserActiveCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); user.IsActive = !user.IsActive; // Toggle await _userRepository.UpdateAsync(user, cancellationToken); } } ``` ### Pattern 3: Cleanup Operation ```csharp public record CleanupExpiredSessionsCommand { } public class CleanupExpiredSessionsCommandHandler : ICommandHandler { private readonly ISessionRepository _sessions; private readonly ILogger _logger; public async Task HandleAsync(CleanupExpiredSessionsCommand command, CancellationToken cancellationToken) { var expiredSessions = await _sessions.GetExpiredAsync(cancellationToken); _logger.LogInformation("Cleaning up {Count} expired sessions", expiredSessions.Count); foreach (var session in expiredSessions) { await _sessions.DeleteAsync(session, cancellationToken); } _logger.LogInformation("Cleanup complete"); } } ``` ### Pattern 4: Notification Command ```csharp public record SendWelcomeEmailCommand { public int UserId { get; init; } } public class SendWelcomeEmailCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly IEmailService _emailService; public async Task HandleAsync(SendWelcomeEmailCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); await _emailService.SendWelcomeEmailAsync(user.Email, user.Name, cancellationToken); } } ``` ## When to Use Basic Commands ### ✅ Use Basic Commands When: - **Delete operations** - Removing data - **Update operations** - Changing existing data without needing confirmation - **Side effects** - Sending emails, publishing events - **Fire-and-forget** - No result needed - **Idempotent operations** - Can be safely retried ### ❌ Use Commands with Results When: - Need to return created ID - Need to return operation status - Need to return validation results - Need to return computed data ## Error Handling ### Not Found ```csharp public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); await _userRepository.DeleteAsync(user, cancellationToken); } ``` **HTTP Response:** `404 Not Found` ### Business Rule Violation ```csharp public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); // Business rule: Cannot delete users with active orders if (await _orders.UserHasActiveOrdersAsync(user.Id, cancellationToken)) { throw new InvalidOperationException("Cannot delete user with active orders"); } await _userRepository.DeleteAsync(user, cancellationToken); } ``` **HTTP Response:** `400 Bad Request` ### Concurrent Modification ```csharp public async Task HandleAsync(UpdateUserCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); // Check concurrency token (optimistic concurrency) if (user.RowVersion != command.RowVersion) { throw new DbUpdateConcurrencyException("User was modified by another user"); } user.Name = command.Name; await _userRepository.UpdateAsync(user, cancellationToken); } ``` **HTTP Response:** `409 Conflict` ## Best Practices ### ✅ DO - Throw exceptions for errors (framework handles HTTP status codes) - Log important operations - Validate business rules - Use CancellationToken - Return `Task` (not `Task` or similar) ### ❌ DON'T - Don't return null or void - return Task - Don't swallow exceptions - Don't forget to check if entity exists - Don't skip business rule validation ## See Also - [Commands with Results](commands-with-results.md) - When to return data - [Command Registration](command-registration.md) - Registration patterns - [Validation](../validation/README.md) - Input validation