| name | plastic |
| description | USE FOR: Implementing business workflows as command pipelines using the Plastic library's command pattern. Use when orchestrating multi-step operations with validation, execution, and rollback semantics.
DO NOT USE FOR: Simple input validation (use FluentValidation), individual guard clauses (use CommunityToolkit Guard), or full event-sourcing/CQRS frameworks (use MediatR, Wolverine, or MassTransit).
|
| license | MIT |
| metadata | {"displayName":"Plastic","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"Plastic GitHub Repository","url":"https://github.com/sang-hyeon/Plastic"}] |
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.
namespace MyApp.Commands;
public interface ICommand
{
bool IsValid { get; }
string? ErrorMessage { get; }
void Validate();
void Execute();
}
public interface ICommand<T> : 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 <> : , <>
{
T Result { ; ; } = !;
}
Concrete Command with Validation
Implement commands that validate preconditions before executing business logic.
namespace MyApp.Commands;
public class CreateOrderCommand : CommandBase<Order>
{
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();
;
}
( item _request.Items)
{
stock = _inventoryService.GetStock(item.ProductId);
(stock < item.Quantity)
{
Invalidate( +
);
;
}
}
}
{
order = Order
{
CustomerId = _request.CustomerId,
Items = _request.Items.Select(i => 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);
( item order.Items)
{
_inventoryService.DecrementStock(item.ProductId, item.Quantity);
}
Result = order;
}
}
Command Pipeline
Compose commands into a pipeline that executes them sequentially with validation.
namespace MyApp.Pipelines;
public class CommandPipeline
{
private readonly List<ICommand> _commands = new();
private readonly List<ICommand> _executedCommands = new();
public CommandPipeline Add(ICommand command)
{
_commands.Add(command);
return this;
}
public PipelineResult Execute()
{
foreach (var command in _commands)
{
command.Validate();
if (!command.IsValid)
{
return PipelineResult.Failed(command.ErrorMessage!);
}
}
try
{
foreach (var command in _commands)
{
command.Execute();
_executedCommands.Add(command);
}
return PipelineResult.Succeeded();
}
catch (Exception ex)
{
Rollback();
return PipelineResult.Failed($"Pipeline failed: {ex.Message}");
}
}
private void Rollback()
{
for (int i = _executedCommands.Count - ; i >= ; i--)
{
(_executedCommands[i] IRollbackable rollbackable)
{
rollbackable.Rollback();
}
}
}
}
{
;
}
{
Success { ; ; }
? Error { ; ; }
=> () { Success = };
=> () { Success = , Error = error };
}
Rollbackable Command
Implement commands that can undo their effects on failure.
namespace MyApp.Commands;
public class ReserveInventoryCommand : CommandBase, IRollbackable
{
private readonly IInventoryService _inventoryService;
private readonly List<OrderItem> _items;
private readonly List<(string ProductId, int Quantity)> _reservations = new();
public ReserveInventoryCommand(
IInventoryService inventoryService,
List<OrderItem> 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));
}
}
()
{
( (productId, quantity) _reservations)
{
_inventoryService.IncrementStock(productId, quantity);
}
_reservations.Clear();
}
}
: <>,
{
IPaymentGateway _paymentGateway;
_amount;
_paymentMethodId;
? _transactionId;
{
_paymentGateway = paymentGateway;
_amount = amount;
_paymentMethodId = paymentMethodId;
}
{
(_amount <= )
Invalidate();
(.IsNullOrWhiteSpace(_paymentMethodId))
Invalidate();
}
{
_transactionId = _paymentGateway.Charge(_paymentMethodId, _amount);
Result = _transactionId;
}
{
(_transactionId )
{
_paymentGateway.Refund(_transactionId);
_transactionId = ;
}
}
}
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
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.