# Plastic ## Overview Plastic is a lightweight .NET library that implements the command pattern for building validation and execution pipelines. Commands encapsulate a unit of work with validation logic and execution logic. Commands can be composed into pipelines where each command executes sequentially, with built-in support for validation before execution and rollback on failure. Plastic is useful for orchestrating multi-step business workflows where each step may have preconditions and side effects that need to be undone if a later step fails. ## Basic Command Implementation Create commands by implementing validation and execution logic. ```csharp namespace MyApp.Commands; public interface ICommand { bool IsValid { get; } string? ErrorMessage { get; } void Validate(); void Execute(); } public interface ICommand : ICommand { T Result { get; } } public abstract class CommandBase : ICommand { public bool IsValid { get; private set; } = true; public string? ErrorMessage { get; private set; } protected void Invalidate(string message) { IsValid = false; ErrorMessage = message; } public abstract void Validate(); public abstract void Execute(); } public abstract class CommandBase : CommandBase, ICommand { public T Result { get; protected set; } = default!; } ``` ## Concrete Command with Validation Implement commands that validate preconditions before executing business logic. ```csharp namespace MyApp.Commands; public class CreateOrderCommand : CommandBase { private readonly IOrderRepository _orderRepo; private readonly IInventoryService _inventoryService; private readonly ICustomerRepository _customerRepo; private readonly CreateOrderRequest _request; public CreateOrderCommand( IOrderRepository orderRepo, IInventoryService inventoryService, ICustomerRepository customerRepo, CreateOrderRequest request) { _orderRepo = orderRepo; _inventoryService = inventoryService; _customerRepo = customerRepo; _request = request; } public override void Validate() { if (_request.Items.Count == 0) { Invalidate("Order must contain at least one item."); return; } if (_request.Items.Any(i => i.Quantity <= 0)) { Invalidate("All item quantities must be greater than zero."); return; } var customer = _customerRepo.GetById(_request.CustomerId); if (customer is null) { Invalidate($"Customer {_request.CustomerId} not found."); return; } if (!customer.IsActive) { Invalidate($"Customer '{customer.Name}' is not active."); return; } foreach (var item in _request.Items) { var stock = _inventoryService.GetStock(item.ProductId); if (stock < item.Quantity) { Invalidate($"Insufficient stock for {item.ProductId}. " + $"Available: {stock}, Requested: {item.Quantity}."); return; } } } public override void Execute() { var order = new Order { CustomerId = _request.CustomerId, Items = _request.Items.Select(i => new OrderItem { ProductId = i.ProductId, Quantity = i.Quantity, UnitPrice = i.UnitPrice }).ToList(), CreatedAt = DateTime.UtcNow, Status = OrderStatus.Pending }; order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice); _orderRepo.Insert(order); foreach (var item in order.Items) { _inventoryService.DecrementStock(item.ProductId, item.Quantity); } Result = order; } } ``` ## Command Pipeline Compose commands into a pipeline that executes them sequentially with validation. ```csharp namespace MyApp.Pipelines; public class CommandPipeline { private readonly List _commands = new(); private readonly List _executedCommands = new(); public CommandPipeline Add(ICommand command) { _commands.Add(command); return this; } public PipelineResult Execute() { // Validate all commands first foreach (var command in _commands) { command.Validate(); if (!command.IsValid) { return PipelineResult.Failed(command.ErrorMessage!); } } // Execute all commands try { foreach (var command in _commands) { command.Execute(); _executedCommands.Add(command); } return PipelineResult.Succeeded(); } catch (Exception ex) { // Rollback executed commands in reverse order Rollback(); return PipelineResult.Failed($"Pipeline failed: {ex.Message}"); } } private void Rollback() { for (int i = _executedCommands.Count - 1; i >= 0; i--) { if (_executedCommands[i] is IRollbackable rollbackable) { rollbackable.Rollback(); } } } } public interface IRollbackable { void Rollback(); } public class PipelineResult { public bool Success { get; private set; } public string? Error { get; private set; } public static PipelineResult Succeeded() => new() { Success = true }; public static PipelineResult Failed(string error) => new() { Success = false, Error = error }; } ``` ## Rollbackable Command Implement commands that can undo their effects on failure. ```csharp namespace MyApp.Commands; public class ReserveInventoryCommand : CommandBase, IRollbackable { private readonly IInventoryService _inventoryService; private readonly List _items; private readonly List<(string ProductId, int Quantity)> _reservations = new(); public ReserveInventoryCommand( IInventoryService inventoryService, List items) { _inventoryService = inventoryService; _items = items; } public override void Validate() { foreach (var item in _items) { var stock = _inventoryService.GetStock(item.ProductId); if (stock < item.Quantity) { Invalidate($"Cannot reserve {item.Quantity} of {item.ProductId}. Only {stock} available."); return; } } } public override void Execute() { foreach (var item in _items) { _inventoryService.DecrementStock(item.ProductId, item.Quantity); _reservations.Add((item.ProductId, item.Quantity)); } } public void Rollback() { foreach (var (productId, quantity) in _reservations) { _inventoryService.IncrementStock(productId, quantity); } _reservations.Clear(); } } public class ChargePaymentCommand : CommandBase, IRollbackable { private readonly IPaymentGateway _paymentGateway; private readonly decimal _amount; private readonly string _paymentMethodId; private string? _transactionId; public ChargePaymentCommand( IPaymentGateway paymentGateway, decimal amount, string paymentMethodId) { _paymentGateway = paymentGateway; _amount = amount; _paymentMethodId = paymentMethodId; } public override void Validate() { if (_amount <= 0) Invalidate("Payment amount must be positive."); if (string.IsNullOrWhiteSpace(_paymentMethodId)) Invalidate("Payment method is required."); } public override void Execute() { _transactionId = _paymentGateway.Charge(_paymentMethodId, _amount); Result = _transactionId; } public void Rollback() { if (_transactionId is not null) { _paymentGateway.Refund(_transactionId); _transactionId = null; } } } ``` ## Plastic vs Other Command/Validation Patterns | Feature | Plastic (Command) | Peasy (Rules) | MediatR (Pipeline) | Saga Pattern | |---|---|---|---|---| | Unit of work | Command | Rule | Request Handler | Step | | Validation | Per-command | Per-rule | Pipeline behavior | Per-step | | Execution | Sequential pipeline | Rule chain | Handler + behaviors | Orchestrator | | Rollback | IRollbackable | Not built-in | Not built-in | Compensating actions | | Async | Optional | Built-in | Built-in | Built-in | | Composition | Pipeline.Add() | IfValidThenValidate | DI pipeline | State machine | ## Best Practices 1. **Implement `Validate()` to check all preconditions before any side effects** and `Execute()` to perform the actual work, never mixing validation logic into `Execute()`; this separation ensures that calling `Validate()` alone is safe and idempotent for preview or dry-run scenarios. 2. **Implement `IRollbackable` on every command that produces side effects** (database writes, payment charges, inventory decrements, external API calls) and track the state needed to undo those effects, so the pipeline can restore consistency when a later command fails. 3. **Store undo state in the command instance** (e.g., `_reservations`, `_transactionId`) during `Execute()` so that `Rollback()` has the information it needs; do not rely on re-querying the database for rollback data because the state may have changed between execution and rollback. 4. **Order commands in the pipeline from least-side-effectful to most-side-effectful** (validate-only commands first, then database writes, then external API calls like payment charges last), minimizing the number of commands that need rollback when a late-stage command fails. 5. **Return `PipelineResult` objects from the pipeline rather than throwing exceptions** for business-rule failures, because pipeline failures are expected outcomes that should be communicated as structured responses; reserve exceptions for unexpected infrastructure errors. 6. **Create separate command classes for each discrete side effect** (e.g., `ReserveInventoryCommand`, `ChargePaymentCommand`, `SendConfirmationEmailCommand`) rather than combining multiple operations in a single command, so that each command's rollback logic is clear and self-contained. 7. **Unit test each command's `Validate()` and `Execute()` independently** by mocking dependencies and asserting on `IsValid`, `ErrorMessage`, and `Result`, then test the pipeline composition with integration tests that verify the full sequence including rollback. 8. **Do not reuse command instances across multiple pipeline executions** because commands store internal state (validation results, execution state, rollback data) from the previous run; create fresh command instances for each pipeline execution to avoid stale state leaking between runs. 9. **Log the start and completion of each command in the pipeline** with the command class name and duration, so that pipeline failures can be diagnosed by examining which command succeeded and which failed, without stepping through the entire pipeline in a debugger. 10. **Use the pipeline pattern for multi-step workflows (order fulfillment, user registration, batch processing)** that involve multiple repositories or external services, but do not use it for single-step operations where a simple service method with Guard clauses is sufficient.