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

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