FromEvery Attribute
The FromEvery attribute allows you to set properties that should be updated for every event that the projection processes, regardless of the specific event type. This is particularly useful for tracking metadata like timestamps, sequence numbers, or other audit fields that should be updated whenever any event affects the read model.
Overview
Unlike event-specific attributes like SetFrom<TEvent> which only apply when a specific event type occurs, FromEvery applies to all events that are part of the projection. This ensures that certain properties are always kept up-to-date across all event types.
Basic Usage
Mapping from Event Context Properties
The most common use case is to map from event context properties like the occurrence timestamp:
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
public record InventoryStatus(
[Key]
Guid Id,
[SetFrom<ProductRegisteredInInventory>(nameof(ProductRegisteredInInventory.ProductName))]
string ProductName,
[AddFrom<ItemsAddedToInventory>(nameof(ItemsAddedToInventory.Quantity))]
[SubtractFrom<ItemsRemovedFromInventory>(nameof(ItemsRemovedFromInventory.Quantity))]
int CurrentStock,
[FromEvery(contextProperty: nameof(EventContext.Occurred))]
DateTimeOffset LastUpdated);
In this example, LastUpdated will be set to the event's occurrence time for every event that affects this inventory item, whether it's a product registration, items being added, or items being removed.
Available Event Context Properties
You can map to any property available on the EventContext:
public record AuditableEntity(
[Key] Guid Id,
[FromEvery(contextProperty: nameof(EventContext.Occurred))]
DateTimeOffset LastModified,
[FromEvery(contextProperty: nameof(EventContext.SequenceNumber))]
ulong LastEventSequence,
[FromEvery(contextProperty: nameof(EventContext.CorrelationId))]
string LastCorrelationId);
Mapping from Event Properties
You can also map from properties that exist on every event type in your projection. When you specify a property name, the attribute will attempt to read that property from each event:
public record OrderStatus(
[Key] Guid Id,
[FromEvery(property: "Status")]
string CurrentStatus);
This assumes that all events in the projection have a Status property. If an event doesn't have the specified property, the mapping is skipped for that event.
Convention-Based Mapping
If you don't specify either property or contextProperty, the attribute will use the read model property name to look for a matching property on each event:
public record Product(
[Key] Guid Id,
[FromEvery] // Looks for 'Version' property on all events
int Version);
Combining with Other Attributes
FromEvery works seamlessly with other projection attributes. Each event can trigger both event-specific mappings and the FromEvery mapping:
public record UserProfile(
[Key] Guid Id,
[SetFrom<UserCreated>(nameof(UserCreated.Name))]
[SetFrom<UserNameChanged>(nameof(UserNameChanged.NewName))]
string Name,
[SetFrom<UserCreated>(nameof(UserCreated.Email))]
[SetFrom<UserEmailChanged>(nameof(UserEmailChanged.NewEmail))]
string Email,
[FromEvery(contextProperty: nameof(EventContext.Occurred))]
DateTimeOffset LastUpdated);
When a UserNameChanged event occurs:
- The
Nameproperty is updated viaSetFrom<UserNameChanged> - The
LastUpdatedproperty is updated viaFromEvery
Use Cases
Audit Timestamps
Track when a read model was last modified:
[FromEvery(contextProperty: nameof(EventContext.Occurred))]
DateTimeOffset LastModified
Event Sequencing
Keep track of the last event sequence number:
[FromEvery(contextProperty: nameof(EventContext.SequenceNumber))]
ulong LastEventSequence
Correlation Tracking
Maintain the correlation ID of the last operation:
[FromEvery(contextProperty: nameof(EventContext.CorrelationId))]
string LastOperationId
Causation Tracking
Track who or what caused the last change:
[FromEvery(contextProperty: nameof(EventContext.CausedBy))]
string LastModifiedBy
Comparison with Fluent API
The FromEvery attribute provides the same functionality as the FromEvery() method in fluent projections:
Fluent Projection:
public class InventoryStatusProjection : IProjectionFor<InventoryStatus>
{
public void Define(IProjectionBuilderFor<InventoryStatus> builder) => builder
.AutoMap()
.FromEvery(_ => _
.Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred))
.From<ProductRegisteredInInventory>()
.From<ItemsAddedToInventory>()
.From<ItemsRemovedFromInventory>();
}
Model-Bound Projection:
[FromEvent<ProductRegisteredInInventory>]
public record InventoryStatus(
[Key] Guid Id,
string ProductName,
int CurrentStock,
[FromEvery(contextProperty: nameof(EventContext.Occurred))] DateTimeOffset LastUpdated);
Both approaches produce identical results, but the model-bound approach keeps the metadata definition directly on the read model.
Best Practices
- Use for metadata:
FromEveryis ideal for tracking metadata like timestamps, sequence numbers, and correlation IDs - Avoid business logic: Don't use
FromEveryfor business-critical properties that should only change under specific conditions - Context properties preferred: When possible, prefer mapping from event context properties over event properties for consistency
- Clear naming: Use property names that clearly indicate they're updated by all events (e.g.,
LastUpdated,LastModified)
Limitations
- The
FromEveryattribute applies to all events in the projection - you cannot selectively exclude specific event types - When mapping from event properties, if an event doesn't have the specified property, the mapping is silently skipped
- Only one
FromEveryattribute can be applied per property (multiple updates to the same property from different event context fields should use the fluent API instead)