302 lines
7.7 KiB
Markdown
302 lines
7.7 KiB
Markdown
# 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<TCommand>` 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<in TCommand>
|
|
{
|
|
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<DeleteUserCommand>
|
|
{
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly ILogger<DeleteUserCommandHandler> _logger;
|
|
|
|
public DeleteUserCommandHandler(
|
|
IUserRepository userRepository,
|
|
ILogger<DeleteUserCommandHandler> 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<DeleteUserCommand, DeleteUserCommandHandler>();
|
|
```
|
|
|
|
## 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<UpdateUserEmailCommand>
|
|
{
|
|
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<ToggleUserActiveCommand>
|
|
{
|
|
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<CleanupExpiredSessionsCommand>
|
|
{
|
|
private readonly ISessionRepository _sessions;
|
|
private readonly ILogger<CleanupExpiredSessionsCommandHandler> _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<SendWelcomeEmailCommand>
|
|
{
|
|
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<unit>` 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
|