Projection with composite keys
When a single property isn’t sufficient to uniquely identify a projection instance, you can use composite keys made up of multiple values. This is useful for multi-tenant scenarios, hierarchical data, or when you need complex keys.
Defining a composite key
Section titled “Defining a composite key”Use UsingCompositeKey<>() to define a key made up of multiple properties:
public class OrderProjection : IProjectionFor<Order>{ public void Define(IProjectionBuilderFor<Order> builder) => builder .AutoMap() .From<OrderCreated>(_ => _ .UsingCompositeKey<OrderKey>(_ => _ .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber))) .From<OrderShipped>(_ => _ .UsingCompositeKey<OrderKey>(_ => _ .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber)));}Composite key type
Section titled “Composite key type”Define a record or class to represent your composite key:
public record OrderKey(string CustomerId, string OrderNumber);Read model with composite key
Section titled “Read model with composite key”The read model’s Id property should match your composite key type:
public record Order( OrderKey Id, string CustomerName, DateTimeOffset OrderDate, DateTimeOffset? ShippedDate);Composite keys with event context
Section titled “Composite keys with event context”You can combine event properties with event context properties in composite keys:
public class AuditProjection : IProjectionFor<AuditEntry>{ public void Define(IProjectionBuilderFor<AuditEntry> builder) => builder .AutoMap() .From<UserAction>(_ => _ .UsingCompositeKey<AuditKey>(_ => _ .Set(k => k.UserId).To(e => e.UserId) .Set(k => k.Timestamp).ToEventContextProperty(c => c.Occurred)));}The corresponding key and read model:
public record AuditKey(string UserId, DateTimeOffset Timestamp);
public record AuditEntry( AuditKey Id, string Action, string Details);Composite keys in child collections
Section titled “Composite keys in child collections”Composite keys work with child collections too:
.Children(m => m.OrderItems, children => children .IdentifiedBy(e => e.ItemId) .AutoMap() .From<ItemAddedToOrder>(_ => _ .UsingCompositeKey<ItemKey>(_ => _ .Set(k => k.ProductId).To(e => e.ProductId) .Set(k => k.Variant).To(e => e.Variant))))Joins with composite keys
Section titled “Joins with composite keys”Composite keys can be used in join scenarios:
.AutoMap().Join<ProductUpdated>(j => j .On(m => m.ProductKey)) // Join on composite key propertyEvent definitions
Section titled “Event definitions”[EventType]public record OrderCreated( string CustomerId, string OrderNumber, string CustomerName, DateTimeOffset OrderDate);
[EventType]public record OrderShipped( string CustomerId, string OrderNumber, DateTimeOffset ShippedDate);
[EventType]public record UserAction( string UserId, string ActionType, string Details);Key composition rules
Section titled “Key composition rules”- Consistent structure: All events that target the same projection must use the same composite key structure
- Immutable parts: Key components should not change during the lifetime of a projection instance
- Uniqueness: The combination of all key parts must uniquely identify each projection instance
- Type safety: Key components are strongly typed and validated at compile time
Performance considerations
Section titled “Performance considerations”- Index efficiency: Composite keys create complex indexes in the underlying storage
- Query patterns: Consider how you’ll query the data when designing key structure
- Key size: Larger composite keys use more storage and may impact performance
- Sort order: The order of properties in the composite key affects index efficiency
Composite keys provide powerful flexibility for complex identification scenarios while maintaining type safety and performance.