Ecotone Identifier Mapping
Overview
When a command or event targets an existing aggregate or saga, Ecotone must resolve which instance to load. The identifier is resolved in this priority order:
- •
aggregate.idmetadata — override via message headers (highest priority) - •Native mapping — command/event property name matches
#[Identifier]property name - •
#[TargetIdentifier]— explicit mapping on command/event class property - •
identifierMapping— expression-based mapping on handler attribute - •
identifierMetadataMapping— header-based mapping on handler attribute
Declaring Identifiers
Use #[Identifier] on the identity property of an aggregate or saga:
use Ecotone\Modelling\Attribute\Aggregate;
use Ecotone\Modelling\Attribute\Identifier;
#[Aggregate]
class Order
{
#[Identifier]
private string $orderId;
}
Native ID Mapping (Default)
When the command/event property name matches the aggregate's #[Identifier] property name, mapping is automatic:
class CancelOrder
{
public function __construct(public readonly string $orderId) {}
}
#[Aggregate]
class Order
{
#[Identifier]
private string $orderId;
#[CommandHandler]
public function cancel(CancelOrder $command): void
{
// $orderId resolved automatically from $command->orderId
}
}
aggregate.id Metadata Override
Pass the identifier directly via message metadata. Overrides all other mapping strategies:
$commandBus->sendWithRouting('order.cancel', metadata: ['aggregate.id' => $orderId]);
#[TargetIdentifier] on Commands/Events
When the command/event property name differs from the aggregate/saga identifier:
use Ecotone\Modelling\Attribute\TargetIdentifier;
class OrderStarted
{
public function __construct(
#[TargetIdentifier('orderId')] public string $id
) {}
}
identifierMapping on Handler Attributes
Use expressions to map identifiers from the payload or headers:
#[EventHandler(identifierMapping: ['orderId' => 'payload.id'])]
public function onExisting(OrderStarted $event): void
{
$this->status = $event->status;
}
identifierMetadataMapping on Handler Attributes
Maps aggregate/saga identifiers to specific metadata header names:
#[EventHandler(identifierMetadataMapping: ['orderId' => 'paymentId'])]
public function finishOrder(PaymentWasDoneEvent $event): void
{
$this->status = 'done';
}
Key Rules
- •Command/event properties matching
#[Identifier]names are resolved automatically (native mapping) - •
aggregate.idmetadata overrides all other mapping — use it for routing-key-based commands without message classes - •
#[TargetIdentifier('identifierName')]maps a differently-named property to the aggregate/saga identifier - •
identifierMappingsupports expressions:'payload.propertyName'and"headers['headerName']" - •
identifierMetadataMappingmaps identifiers to header names directly (simpler thanidentifierMappingfor headers) - •You cannot combine
identifierMetadataMappingandidentifierMappingon the same handler - •Use
#[IdentifierMethod('identifierName')]when the identifier value comes from a method rather than a property - •Factory handlers (static) do not need identifier mapping for creation — only action handlers on existing instances do
Additional resources
- •API Reference — Attribute signatures and parameter details for
#[Identifier],#[TargetIdentifier],#[IdentifierMethod],identifierMapping, andidentifierMetadataMapping. Load when you need exact constructor parameters, types, or expression syntax. - •Usage Examples — Complete class implementations for every identifier resolution strategy: aggregates and sagas with native mapping,
aggregate.idoverride (including multiple identifiers),#[TargetIdentifier]full saga flow,identifierMappingfrom payload and headers,identifierMetadataMapping, and#[IdentifierMethod]. Load when you need full, copy-paste-ready class definitions. - •Testing Patterns — EcotoneLite test methods for each identifier mapping strategy: native mapping,
aggregate.idoverride,#[TargetIdentifier]with sagas,identifierMappingfrom payload, andidentifierMappingfrom headers. Load when writing tests for identifier resolution.