6.2 KiB
6.2 KiB
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
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)
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)
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:
{
"orderId": 456,
"totalAmount": 99.99,
"orderNumber": "ORD-2025-001",
"createdAt": "2025-01-15T10:30:00Z"
}
3. Status/Summary Objects
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:
{
"totalRecords": 100,
"successCount": 95,
"errorCount": 5,
"errors": [
"user1@example.com: Email already exists",
"user2@example.com: Invalid email format"
]
}
4. Boolean (Success/Failure)
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
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 - Commands without results
- Command Registration - How to register
- Best Practices: Command Design