dotnet-cqrs/docs/core-features/commands/commands-with-results.md

248 lines
6.2 KiB
Markdown

# Commands with Results
Commands that return data after execution.
## Overview
Commands with results use the `ICommandHandler<TCommand, TResult>` interface and return data such as IDs, status information, or computed results.
## Interface
```csharp
public interface ICommandHandler<in TCommand, TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
```
## When to Return Data
### ✅ Return Data When:
- **Created IDs** - Return newly created entity IDs
- **Operation status** - Return success/failure details
- **Computed results** - Return calculated values
- **Confirmation data** - Return what was created/updated
- **Batch results** - Return summary of batch operations
### ❌ Don't Return:
- Domain entities directly (use DTOs)
- Sensitive data
- Unnecessary data
## Common Return Types
### 1. Primitive Types (IDs)
```csharp
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
var user = new User { Name = command.Name, Email = command.Email };
await _context.Users.AddAsync(user, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return user.Id; // Return ID
}
}
```
**HTTP Response:**
```
200 OK
Content-Type: application/json
123
```
### 2. DTOs (Confirmation)
```csharp
public record CreateOrderResult
{
public int OrderId { get; init; }
public decimal TotalAmount { get; init; }
public string OrderNumber { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; }
}
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, CreateOrderResult>
{
public async Task<CreateOrderResult> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
{
var order = new Order
{
CustomerId = command.CustomerId,
Items = command.Items,
TotalAmount = CalculateTotal(command.Items)
};
await _orders.AddAsync(order, cancellationToken);
return new CreateOrderResult
{
OrderId = order.Id,
TotalAmount = order.TotalAmount,
OrderNumber = order.OrderNumber,
CreatedAt = order.CreatedAt
};
}
}
```
**HTTP Response:**
```json
{
"orderId": 456,
"totalAmount": 99.99,
"orderNumber": "ORD-2025-001",
"createdAt": "2025-01-15T10:30:00Z"
}
```
### 3. Status/Summary Objects
```csharp
public record ImportResult
{
public int TotalRecords { get; init; }
public int SuccessCount { get; init; }
public int ErrorCount { get; init; }
public List<string> Errors { get; init; } = new();
}
public class ImportUsersCommandHandler : ICommandHandler<ImportUsersCommand, ImportResult>
{
public async Task<ImportResult> HandleAsync(ImportUsersCommand command, CancellationToken cancellationToken)
{
var result = new ImportResult { TotalRecords = command.Users.Count };
var successCount = 0;
var errors = new List<string>();
foreach (var userDto in command.Users)
{
try
{
await CreateUserAsync(userDto, cancellationToken);
successCount++;
}
catch (Exception ex)
{
errors.Add($"{userDto.Email}: {ex.Message}");
}
}
return result with
{
SuccessCount = successCount,
ErrorCount = errors.Count,
Errors = errors
};
}
}
```
**HTTP Response:**
```json
{
"totalRecords": 100,
"successCount": 95,
"errorCount": 5,
"errors": [
"user1@example.com: Email already exists",
"user2@example.com: Invalid email format"
]
}
```
### 4. Boolean (Success/Failure)
```csharp
public class ActivateUserCommandHandler : ICommandHandler<ActivateUserCommand, bool>
{
public async Task<bool> HandleAsync(ActivateUserCommand command, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken);
if (user == null)
return false; // User not found
if (user.IsActive)
return true; // Already active
user.IsActive = true;
user.ActivatedAt = DateTime.UtcNow;
await _userRepository.UpdateAsync(user, cancellationToken);
return true;
}
}
```
### 5. Complex Objects
```csharp
public record PaymentResult
{
public string PaymentId { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public decimal Amount { get; init; }
public string TransactionId { get; init; } = string.Empty;
public DateTime ProcessedAt { get; init; }
}
public class ProcessPaymentCommandHandler : ICommandHandler<ProcessPaymentCommand, PaymentResult>
{
public async Task<PaymentResult> HandleAsync(ProcessPaymentCommand command, CancellationToken cancellationToken)
{
var payment = await _paymentService.ChargeAsync(
command.PaymentMethod,
command.Amount,
cancellationToken);
await _orders.UpdatePaymentAsync(command.OrderId, payment.Id, cancellationToken);
return new PaymentResult
{
PaymentId = payment.Id,
Status = payment.Status,
Amount = payment.Amount,
TransactionId = payment.TransactionId,
ProcessedAt = payment.ProcessedAt
};
}
}
```
## Best Practices
### ✅ DO
- Return IDs for created entities
- Return DTOs, not domain entities
- Include enough data for client confirmation
- Return operation status for batch operations
- Document what's returned in XML comments
### ❌ DON'T
- Return entire domain entities
- Return sensitive data (passwords, tokens)
- Return more data than needed
- Return null for success cases
- Return complex nested structures
## See Also
- [Basic Commands](basic-commands.md) - Commands without results
- [Command Registration](command-registration.md) - How to register
- [Best Practices: Command Design](../../best-practices/command-design.md)