Response Value Handlers
Command handlers can return values that need to be processed by the command pipeline. The Arc provides a flexible system for handling these return values through Response Value Handlers.
Automatic Response Handling
When a command handler returns a value, the command pipeline follows this logic:
- Check for Value Handlers: The pipeline first checks if any registered response value handlers can handle the returned value using their
CanHandlemethod - Process with Handler: If a value handler can handle the value, it processes the value and returns a
CommandResult - Automatic Response Creation: If no value handler can handle the value, the pipeline automatically creates a
CommandResult<T>with the returned value as the response
This means that command handlers can return any type of value, and it will either be processed by a specific handler or automatically become the command response.
Built-in Value Handlers
Out-of-the-box Cratis Arc comes with the following value handlers:
| Type | Description |
|---|---|
| ValidationResultResponseValueHandler | Responds to Cratis ValidationResult object and adds it to the command result |
Command Handler Return Patterns
Single Value Return
[Command]
public record CreateUser(string Name, string Email)
{
public Guid Handle()
{
var userId = Guid.NewGuid();
// This will automatically create CommandResult<Guid> with userId as response
return userId;
}
}
Result Return
using Cratis.Arc.Validation;
using OneOf;
[Command]
public record CreateUser(string Name, string Email)
{
public Result<ValidationResult, UserId> Handle()
{
if (!IsValidEmail(Email))
{
return ValidationResult.Error("Invalid email address");
}
var userId = new UserId(Guid.NewGuid());
// If no handler can process UserId, it becomes CommandResult<UserId>
// If ValidationResultResponseValueHandler processes ValidationResult, it affects the command result
return userId;
}
}
Tuple Return
[Command]
public record CreateUser(string Name, string Email)
{
public (UserId, AuditInfo) Handle()
{
var userId = new UserId(Guid.NewGuid());
var auditInfo = new AuditInfo(DateTime.UtcNow, "system");
// Each value is checked against available handlers
// If no handler processes userId, it becomes the response
// If a handler processes auditInfo, it affects the command result
return (userId, auditInfo);
}
}
Creating Custom Value Handlers
You can create custom response value handlers by implementing the ICommandResponseValueHandler interface:
public class AuditInfoResponseValueHandler : ICommandResponseValueHandler
{
public bool CanHandle(CommandContext commandContext, object value)
{
return value is AuditInfo;
}
public Task<CommandResult> Handle(CommandContext commandContext, object value)
{
var auditInfo = (AuditInfo)value;
// Perform audit logging
LogAuditEvent(auditInfo);
// Return success - this doesn't affect the command response
return Task.FromResult(CommandResult.Success(commandContext.CorrelationId));
}
}
The Arc will automatically discover and register custom value handlers in the command pipeline.
Response Object Availability
When implementing a command response value handler, the CommandContext.Response property contains the response object returned by the command handler, if any. This property can be null in the following scenarios:
- The command handler didn't return anything (void method)
- The command handler returned
null - The command handler returned a single value that has no corresponding value handler (in which case that value becomes the automatic response)
Tuple Processing Behavior
When a command handler returns a tuple, the command pipeline intelligently processes each value:
- Each value is evaluated against all available response value handlers using their
CanHandlemethod - Values with handlers are processed by their respective response value handlers
- Values without handlers are considered potential response values:
- If exactly one value has no handler, it becomes the response (available in
CommandContext.Response) - If multiple values have no handlers, a
MultipleUnhandledTupleValuesExceptionis thrown - If all values have handlers, no response is set (
CommandContext.Responseremainsnull)
- If exactly one value has no handler, it becomes the response (available in
Result Processing Behavior
When a command handler returns a Result<TError, TSuccess> or OneOf<T1, T2, ...> value:
- The inner value is extracted from the Result/OneOf wrapper
- Value handlers are checked using the
CanHandlemethod on the inner value - If a handler can process it, the handler processes the value
- If no handler can process it, the inner value automatically becomes a
CommandResult<T>response
Result with Tuple Alternatives
The command pipeline also supports Result types where one or more alternatives are tuples. In this case:
- The inner value is extracted from the Result wrapper
- If the inner value is a tuple, it is processed using the standard tuple processing rules (see above)
- If the inner value is a simple type, it follows the standard Result processing rules
using Cratis.Arc.Validation;
using OneOf;
[Command]
public record CreateOrder(string CustomerId, List<OrderItem> Items)
{
public Result<ValidationResult, (OrderId, OrderCreated)> Handle()
{
if (!IsValidOrder())
{
return ValidationResult.Error("Invalid order");
}
var orderId = OrderId.New();
// Return a tuple with response and event
return (orderId, new OrderCreated(orderId, CustomerId, Items));
}
}
In this example:
- When validation fails, the
ValidationResultis returned and processed by the validation handler - When successful, the tuple is returned:
OrderCreatedis processed by its response value handler (e.g., event handler)OrderIdbecomes the response (assuming it has no handler)
Key Benefits
- Zero Configuration: Values without handlers automatically become responses
- Flexible Processing: Custom handlers can perform side effects (logging, notifications, etc.)
- Type Safety: Automatic responses are properly typed as
CommandResult<T> - Backward Compatibility: Existing value handlers continue to work as before
Example Implementation
public class MyResponseValueHandler : ICommandResponseValueHandler
{
public bool CanHandle(CommandContext commandContext, object value)
{
// The commandContext.Response can be null here
return value is MyValueType;
}
public Task<CommandResult> Handle(CommandContext commandContext, object value)
{
// Access the response if available
var response = commandContext.Response; // This can be null
if (response is not null)
{
// Handle cases where the command returned a response
// This typically happens when the command returns a tuple with unhandled values
}
// Process the value that was returned by the command
var myValue = (MyValueType)value;
// Perform side effects (logging, notifications, etc.)
ProcessValue(myValue);
return Task.FromResult(CommandResult.Success(commandContext.CorrelationId));
}
}
Migration Notes
From Previous Versions
If you were previously relying on the requirement that all returned values must have corresponding value handlers, this is no longer necessary. Values without handlers will now automatically become command responses, which provides:
- Simplified Development: No need to create handlers for simple response values
- Better Developer Experience: Commands can return domain objects directly
- Reduced Boilerplate: Less code needed for basic response scenarios
Backward Compatibility
All existing value handlers continue to work exactly as before. The new automatic response creation only applies to values that have no corresponding handlers, ensuring full backward compatibility.