# Commands Overview Commands represent write operations that change system state in your application. ## What are Commands? Commands are **imperative requests** to perform an action that changes the state of your system. They encapsulate all the data needed to perform an operation. **Characteristics:** - ✅ **Imperative names** - CreateUser, PlaceOrder, CancelSubscription - ✅ **Change state** - Modify database, send emails, publish events - ✅ **May return data** - Often return IDs or confirmation data - ✅ **Validated** - Input validation before execution - ✅ **Can fail** - Business rules may prevent execution - ✅ **Not idempotent** - Executing twice may have different results ## Command Types ### Commands Without Results Commands that perform an action but don't return data: ```csharp public record DeleteUserCommand { public int UserId { get; init; } } public class DeleteUserCommandHandler : ICommandHandler { public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { // Delete user logic // No return value } } ``` **HTTP Response:** `204 No Content` ### Commands With Results Commands that return data (typically IDs or confirmation): ```csharp public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } public class CreateUserCommandHandler : ICommandHandler { public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // Create user logic return userId; } } ``` **HTTP Response:** `200 OK` with the result ## Basic Command Example ### Step 1: Define the Command ```csharp // Commands/CreateUserCommand.cs namespace MyApp.Commands; public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; public int Age { get; init; } } ``` ### Step 2: Create the Handler ```csharp using Svrnty.CQRS.Abstractions; using MyApp.Domain.Entities; using MyApp.Domain.Repositories; namespace MyApp.Commands; public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly ILogger _logger; public CreateUserCommandHandler( IUserRepository userRepository, ILogger logger) { _userRepository = userRepository; _logger = logger; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { _logger.LogInformation("Creating user: {Email}", command.Email); // Create domain entity var user = new User { Name = command.Name, Email = command.Email, Age = command.Age }; // Save to database var userId = await _userRepository.AddAsync(user, cancellationToken); _logger.LogInformation("User created with ID: {UserId}", userId); return userId; } } ``` ### Step 3: Register the Handler ```csharp // Program.cs builder.Services.AddCommand(); ``` ### Step 4: Test the Command ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{ "name": "Alice Smith", "email": "alice@example.com", "age": 30 }' ``` **Response:** ```json 123 ``` ## Command Documentation ### [Basic Commands](basic-commands.md) Commands without return values (void): - When to use - Implementation patterns - Error handling - HTTP responses (204 No Content) **Topics:** - Delete operations - Update operations without return - Fire-and-forget commands - Idempotent commands ### [Commands with Results](commands-with-results.md) Commands that return data: - When to return data - What to return (IDs, DTOs, confirmation) - Implementation patterns - HTTP responses (200 OK with result) **Topics:** - Create operations (return ID) - Complex operations (return status/summary) - Batch operations (return results array) - Conditional results ### [Command Registration](command-registration.md) How to register commands in DI: - Basic registration - Registration with validators - Registration with workflows - Bulk registration patterns - Extension methods **Topics:** - Service lifetimes (Scoped, Transient, Singleton) - Registration organization - Module pattern - Auto-registration ### [Command Authorization](command-authorization.md) Securing commands with authorization: - ICommandAuthorizationService interface - Role-based authorization - Resource-based authorization - Claims-based authorization **Topics:** - Authorization services - Multiple authorization rules - Combining with ASP.NET Core authorization - Custom authorization logic ### [Command Attributes](command-attributes.md) Controlling command behavior with attributes: - [CommandName] - Custom endpoint names - [IgnoreCommand] - Prevent endpoint generation - [GrpcIgnore] - Skip gRPC generation - Custom attributes **Topics:** - Naming conventions - Endpoint control - Protocol selection - Metadata customization ## Command Patterns ### Pattern 1: Simple CRUD Command ```csharp public record UpdateUserCommand { public int UserId { get; init; } public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } public class UpdateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public async Task HandleAsync(UpdateUserCommand command, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {command.UserId} not found"); user.Name = command.Name; user.Email = command.Email; await _userRepository.UpdateAsync(user, cancellationToken); } } ``` ### Pattern 2: Command with Business Logic ```csharp public record PlaceOrderCommand { public int CustomerId { get; init; } public List Items { get; init; } = new(); } public class PlaceOrderCommandHandler : ICommandHandler { private readonly IOrderRepository _orders; private readonly IInventoryService _inventory; private readonly IPaymentService _payment; public async Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken) { // Business rule: Check inventory foreach (var item in command.Items) { if (!await _inventory.IsAvailableAsync(item.ProductId, item.Quantity)) throw new InvalidOperationException($"Product {item.ProductId} out of stock"); } // Create order var order = new Order { CustomerId = command.CustomerId, Items = command.Items, Status = OrderStatus.Pending }; var orderId = await _orders.AddAsync(order, cancellationToken); // Reserve inventory foreach (var item in command.Items) { await _inventory.ReserveAsync(item.ProductId, item.Quantity, cancellationToken); } return orderId; } } ``` ### Pattern 3: Command with Events ```csharp public class CreateUserCommandHandler : ICommandHandlerWithWorkflow { private readonly IUserRepository _userRepository; public async Task HandleAsync( CreateUserCommand command, UserWorkflow workflow, CancellationToken cancellationToken) { var user = new User { Name = command.Name, Email = command.Email }; var userId = await _userRepository.AddAsync(user, cancellationToken); // Emit domain event workflow.EmitCreated(new UserCreatedEvent { UserId = userId, Name = user.Name, Email = user.Email }); return userId; } } ``` ### Pattern 4: Compensating Command (Saga) ```csharp public record CancelOrderCommand { public int OrderId { get; init; } public string Reason { get; init; } = string.Empty; } public class CancelOrderCommandHandler : ICommandHandler { private readonly IOrderRepository _orders; private readonly IInventoryService _inventory; private readonly IPaymentService _payment; public async Task HandleAsync(CancelOrderCommand command, CancellationToken cancellationToken) { var order = await _orders.GetByIdAsync(command.OrderId, cancellationToken); // Business rule: Can only cancel pending orders if (order.Status != OrderStatus.Pending) throw new InvalidOperationException("Cannot cancel order in this state"); // Compensate: Release inventory foreach (var item in order.Items) { await _inventory.ReleaseAsync(item.ProductId, item.Quantity, cancellationToken); } // Compensate: Refund payment (if paid) if (order.PaymentId != null) { await _payment.RefundAsync(order.PaymentId, cancellationToken); } // Update order status order.Status = OrderStatus.Cancelled; order.CancellationReason = command.Reason; await _orders.UpdateAsync(order, cancellationToken); } } ``` ## Best Practices ### ✅ DO - **Use imperative names** - CreateUser, PlaceOrder, CancelSubscription - **Keep commands simple** - Just data, no logic - **Validate in handlers** - Or use FluentValidation - **Return meaningful data** - IDs, confirmation, summary - **Handle errors gracefully** - Business rule violations, validation errors - **Use CancellationToken** - Enable request cancellation - **Log important actions** - Audit trail - **Make commands immutable** - Use `record` and `init` ### ❌ DON'T - **Don't put logic in commands** - Commands are just data - **Don't return domain entities** - Use DTOs or primitives - **Don't ignore errors** - Handle business rule violations - **Don't make properties mutable** - Use `init` not `set` - **Don't skip validation** - Always validate input - **Don't create fat handlers** - Extract domain services - **Don't forget cancellation** - Always pass CancellationToken ## Common Scenarios ### Scenario 1: Batch Operations ```csharp public record ImportUsersCommand { public List Users { get; init; } = new(); } public class ImportUsersCommandHandler : ICommandHandler { public async Task HandleAsync(ImportUsersCommand command, CancellationToken cancellationToken) { var result = new ImportResult(); foreach (var userDto in command.Users) { try { var user = new User { Name = userDto.Name, Email = userDto.Email }; await _userRepository.AddAsync(user, cancellationToken); result.SuccessCount++; } catch (Exception ex) { result.Errors.Add($"{userDto.Email}: {ex.Message}"); } } return result; } } public class ImportResult { public int SuccessCount { get; set; } public List Errors { get; set; } = new(); } ``` ### Scenario 2: Multi-Step Workflow ```csharp public record ProcessPaymentCommand { public int OrderId { get; init; } public string PaymentMethod { get; init; } = string.Empty; public decimal Amount { get; init; } } public class ProcessPaymentCommandHandler : ICommandHandler { public async Task HandleAsync(ProcessPaymentCommand command, CancellationToken cancellationToken) { // Step 1: Validate order var order = await _orders.GetByIdAsync(command.OrderId, cancellationToken); ValidateOrder(order); // Step 2: Process payment var paymentId = await _payment.ChargeAsync( command.PaymentMethod, command.Amount, cancellationToken); // Step 3: Update order order.PaymentId = paymentId; order.Status = OrderStatus.Paid; await _orders.UpdateAsync(order, cancellationToken); // Step 4: Emit event await _events.PublishAsync(new OrderPaidEvent { OrderId = order.Id }); return new PaymentResult { PaymentId = paymentId, Status = "Success" }; } } ``` ### Scenario 3: Idempotent Command ```csharp public record CreateSubscriptionCommand { public string IdempotencyKey { get; init; } = string.Empty; public int UserId { get; init; } public string Plan { get; init; } = string.Empty; } public class CreateSubscriptionCommandHandler : ICommandHandler { private readonly ISubscriptionRepository _subscriptions; public async Task HandleAsync(CreateSubscriptionCommand command, CancellationToken cancellationToken) { // Check if already processed var existing = await _subscriptions.GetByIdempotencyKeyAsync( command.IdempotencyKey, cancellationToken); if (existing != null) { // Already processed, return existing ID return existing.Id; } // Create new subscription var subscription = new Subscription { IdempotencyKey = command.IdempotencyKey, UserId = command.UserId, Plan = command.Plan }; return await _subscriptions.AddAsync(subscription, cancellationToken); } } ``` ## What's Next? Explore specific command topics: - **[Basic Commands](basic-commands.md)** - Commands without results - **[Commands with Results](commands-with-results.md)** - Commands that return data - **[Command Registration](command-registration.md)** - Registration patterns - **[Command Authorization](command-authorization.md)** - Securing commands - **[Command Attributes](command-attributes.md)** - Controlling behavior ## See Also - [Getting Started: Your First Command](../../getting-started/03-first-command.md) - Step-by-step guide - [Best Practices: Command Design](../../best-practices/command-design.md) - Design patterns - [Architecture: CQRS Pattern](../../architecture/cqrs-pattern.md) - Understanding CQRS - [Validation](../validation/README.md) - Input validation