248 lines
6.2 KiB
Markdown
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)
|