| .. | ||
| basic-commands.md | ||
| command-attributes.md | ||
| command-authorization.md | ||
| command-registration.md | ||
| commands-with-results.md | ||
| README.md | ||
Commands Overview
Commands represent write operations that change system state in your application.
What are Commands?
Commands are imperative requests to perform an action that changes the state of your system. They encapsulate all the data needed to perform an operation.
Characteristics:
- ✅ Imperative names - CreateUser, PlaceOrder, CancelSubscription
- ✅ Change state - Modify database, send emails, publish events
- ✅ May return data - Often return IDs or confirmation data
- ✅ Validated - Input validation before execution
- ✅ Can fail - Business rules may prevent execution
- ✅ Not idempotent - Executing twice may have different results
Command Types
Commands Without Results
Commands that perform an action but don't return data:
public record DeleteUserCommand
{
public int UserId { get; init; }
}
public class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken)
{
// Delete user logic
// No return value
}
}
HTTP Response: 204 No Content
Commands With Results
Commands that return data (typically IDs or confirmation):
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)
{
// Create user logic
return userId;
}
}
HTTP Response: 200 OK with the result
Basic Command Example
Step 1: Define the Command
// Commands/CreateUserCommand.cs
namespace MyApp.Commands;
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
public int Age { get; init; }
}
Step 2: Create the Handler
using Svrnty.CQRS.Abstractions;
using MyApp.Domain.Entities;
using MyApp.Domain.Repositories;
namespace MyApp.Commands;
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
private readonly IUserRepository _userRepository;
private readonly ILogger<CreateUserCommandHandler> _logger;
public CreateUserCommandHandler(
IUserRepository userRepository,
ILogger<CreateUserCommandHandler> logger)
{
_userRepository = userRepository;
_logger = logger;
}
public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
_logger.LogInformation("Creating user: {Email}", command.Email);
// Create domain entity
var user = new User
{
Name = command.Name,
Email = command.Email,
Age = command.Age
};
// Save to database
var userId = await _userRepository.AddAsync(user, cancellationToken);
_logger.LogInformation("User created with ID: {UserId}", userId);
return userId;
}
}
Step 3: Register the Handler
// Program.cs
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
Step 4: Test the Command
curl -X POST http://localhost:5000/api/command/createUser \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Smith",
"email": "alice@example.com",
"age": 30
}'
Response:
123
Command Documentation
Basic Commands
Commands without return values (void):
- When to use
- Implementation patterns
- Error handling
- HTTP responses (204 No Content)
Topics:
- Delete operations
- Update operations without return
- Fire-and-forget commands
- Idempotent commands
Commands with Results
Commands that return data:
- When to return data
- What to return (IDs, DTOs, confirmation)
- Implementation patterns
- HTTP responses (200 OK with result)
Topics:
- Create operations (return ID)
- Complex operations (return status/summary)
- Batch operations (return results array)
- Conditional results
Command Registration
How to register commands in DI:
- Basic registration
- Registration with validators
- Registration with workflows
- Bulk registration patterns
- Extension methods
Topics:
- Service lifetimes (Scoped, Transient, Singleton)
- Registration organization
- Module pattern
- Auto-registration
Command Authorization
Securing commands with authorization:
- ICommandAuthorizationService interface
- Role-based authorization
- Resource-based authorization
- Claims-based authorization
Topics:
- Authorization services
- Multiple authorization rules
- Combining with ASP.NET Core authorization
- Custom authorization logic
Command Attributes
Controlling command behavior with attributes:
- [CommandName] - Custom endpoint names
- [IgnoreCommand] - Prevent endpoint generation
- [GrpcIgnore] - Skip gRPC generation
- Custom attributes
Topics:
- Naming conventions
- Endpoint control
- Protocol selection
- Metadata customization
Command Patterns
Pattern 1: Simple CRUD Command
public record UpdateUserCommand
{
public int UserId { get; init; }
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
public class UpdateUserCommandHandler : ICommandHandler<UpdateUserCommand>
{
private readonly IUserRepository _userRepository;
public async Task HandleAsync(UpdateUserCommand command, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(command.UserId, cancellationToken);
if (user == null)
throw new KeyNotFoundException($"User {command.UserId} not found");
user.Name = command.Name;
user.Email = command.Email;
await _userRepository.UpdateAsync(user, cancellationToken);
}
}
Pattern 2: Command with Business Logic
public record PlaceOrderCommand
{
public int CustomerId { get; init; }
public List<OrderItem> Items { get; init; } = new();
}
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, int>
{
private readonly IOrderRepository _orders;
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
public async Task<int> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
{
// Business rule: Check inventory
foreach (var item in command.Items)
{
if (!await _inventory.IsAvailableAsync(item.ProductId, item.Quantity))
throw new InvalidOperationException($"Product {item.ProductId} out of stock");
}
// Create order
var order = new Order
{
CustomerId = command.CustomerId,
Items = command.Items,
Status = OrderStatus.Pending
};
var orderId = await _orders.AddAsync(order, cancellationToken);
// Reserve inventory
foreach (var item in command.Items)
{
await _inventory.ReserveAsync(item.ProductId, item.Quantity, cancellationToken);
}
return orderId;
}
}
Pattern 3: Command with Events
public class CreateUserCommandHandler : ICommandHandlerWithWorkflow<CreateUserCommand, int, UserWorkflow>
{
private readonly IUserRepository _userRepository;
public async Task<int> HandleAsync(
CreateUserCommand command,
UserWorkflow workflow,
CancellationToken cancellationToken)
{
var user = new User { Name = command.Name, Email = command.Email };
var userId = await _userRepository.AddAsync(user, cancellationToken);
// Emit domain event
workflow.EmitCreated(new UserCreatedEvent
{
UserId = userId,
Name = user.Name,
Email = user.Email
});
return userId;
}
}
Pattern 4: Compensating Command (Saga)
public record CancelOrderCommand
{
public int OrderId { get; init; }
public string Reason { get; init; } = string.Empty;
}
public class CancelOrderCommandHandler : ICommandHandler<CancelOrderCommand>
{
private readonly IOrderRepository _orders;
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
public async Task HandleAsync(CancelOrderCommand command, CancellationToken cancellationToken)
{
var order = await _orders.GetByIdAsync(command.OrderId, cancellationToken);
// Business rule: Can only cancel pending orders
if (order.Status != OrderStatus.Pending)
throw new InvalidOperationException("Cannot cancel order in this state");
// Compensate: Release inventory
foreach (var item in order.Items)
{
await _inventory.ReleaseAsync(item.ProductId, item.Quantity, cancellationToken);
}
// Compensate: Refund payment (if paid)
if (order.PaymentId != null)
{
await _payment.RefundAsync(order.PaymentId, cancellationToken);
}
// Update order status
order.Status = OrderStatus.Cancelled;
order.CancellationReason = command.Reason;
await _orders.UpdateAsync(order, cancellationToken);
}
}
Best Practices
✅ DO
- Use imperative names - CreateUser, PlaceOrder, CancelSubscription
- Keep commands simple - Just data, no logic
- Validate in handlers - Or use FluentValidation
- Return meaningful data - IDs, confirmation, summary
- Handle errors gracefully - Business rule violations, validation errors
- Use CancellationToken - Enable request cancellation
- Log important actions - Audit trail
- Make commands immutable - Use
recordandinit
❌ DON'T
- Don't put logic in commands - Commands are just data
- Don't return domain entities - Use DTOs or primitives
- Don't ignore errors - Handle business rule violations
- Don't make properties mutable - Use
initnotset - Don't skip validation - Always validate input
- Don't create fat handlers - Extract domain services
- Don't forget cancellation - Always pass CancellationToken
Common Scenarios
Scenario 1: Batch Operations
public record ImportUsersCommand
{
public List<UserImportDto> Users { get; init; } = new();
}
public class ImportUsersCommandHandler : ICommandHandler<ImportUsersCommand, ImportResult>
{
public async Task<ImportResult> HandleAsync(ImportUsersCommand command, CancellationToken cancellationToken)
{
var result = new ImportResult();
foreach (var userDto in command.Users)
{
try
{
var user = new User { Name = userDto.Name, Email = userDto.Email };
await _userRepository.AddAsync(user, cancellationToken);
result.SuccessCount++;
}
catch (Exception ex)
{
result.Errors.Add($"{userDto.Email}: {ex.Message}");
}
}
return result;
}
}
public class ImportResult
{
public int SuccessCount { get; set; }
public List<string> Errors { get; set; } = new();
}
Scenario 2: Multi-Step Workflow
public record ProcessPaymentCommand
{
public int OrderId { get; init; }
public string PaymentMethod { get; init; } = string.Empty;
public decimal Amount { get; init; }
}
public class ProcessPaymentCommandHandler : ICommandHandler<ProcessPaymentCommand, PaymentResult>
{
public async Task<PaymentResult> HandleAsync(ProcessPaymentCommand command, CancellationToken cancellationToken)
{
// Step 1: Validate order
var order = await _orders.GetByIdAsync(command.OrderId, cancellationToken);
ValidateOrder(order);
// Step 2: Process payment
var paymentId = await _payment.ChargeAsync(
command.PaymentMethod,
command.Amount,
cancellationToken);
// Step 3: Update order
order.PaymentId = paymentId;
order.Status = OrderStatus.Paid;
await _orders.UpdateAsync(order, cancellationToken);
// Step 4: Emit event
await _events.PublishAsync(new OrderPaidEvent { OrderId = order.Id });
return new PaymentResult
{
PaymentId = paymentId,
Status = "Success"
};
}
}
Scenario 3: Idempotent Command
public record CreateSubscriptionCommand
{
public string IdempotencyKey { get; init; } = string.Empty;
public int UserId { get; init; }
public string Plan { get; init; } = string.Empty;
}
public class CreateSubscriptionCommandHandler : ICommandHandler<CreateSubscriptionCommand, int>
{
private readonly ISubscriptionRepository _subscriptions;
public async Task<int> HandleAsync(CreateSubscriptionCommand command, CancellationToken cancellationToken)
{
// Check if already processed
var existing = await _subscriptions.GetByIdempotencyKeyAsync(
command.IdempotencyKey,
cancellationToken);
if (existing != null)
{
// Already processed, return existing ID
return existing.Id;
}
// Create new subscription
var subscription = new Subscription
{
IdempotencyKey = command.IdempotencyKey,
UserId = command.UserId,
Plan = command.Plan
};
return await _subscriptions.AddAsync(subscription, cancellationToken);
}
}
What's Next?
Explore specific command topics:
- Basic Commands - Commands without results
- Commands with Results - Commands that return data
- Command Registration - Registration patterns
- Command Authorization - Securing commands
- Command Attributes - Controlling behavior
See Also
- Getting Started: Your First Command - Step-by-step guide
- Best Practices: Command Design - Design patterns
- Architecture: CQRS Pattern - Understanding CQRS
- Validation - Input validation