Counters
Model-bound projections provide three counter operations for tracking occurrences and quantities: Increment, Decrement, and Count.
Increment
Section titled “Increment”The Increment attribute increments a numeric property when an event occurs. This is useful for tracking counters that increase with specific events.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbCountersUserLoggedIn;
public record MbCountersUserStatistics( [Key] Guid UserId,
[Increment<MbCountersUserLoggedIn>] int LoginCount);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.CountersUserLoggedIn do use Chronicle.Events.EventType, id: "counters-user-logged-in-v1"
defstruct []end
defmodule MyApp.ReadModels.CountersUserStatistics do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.CountersUserLoggedIn
defstruct [:user_id, login_count: 0]
from CountersUserLoggedIn, set: [user_id: :event_source_id], add: [login_count: 1]endimport { eventType, Guid, increment, readModel } from '@cratis/chronicle';
@eventType()class MbCountersUserLoggedIn {}
@readModel()class MbCountersUserStatistics { id: Guid = Guid.empty;
@increment(MbCountersUserLoggedIn) loginCount = 0;}Each time a UserLoggedIn event occurs, LoginCount is incremented by 1.
Decrement
Section titled “Decrement”The Decrement attribute decrements a numeric property when an event occurs. This is useful for tracking decreasing counters.
[EventType]public record MbCountersUserConnected;
[EventType]public record MbCountersUserDisconnected;
public record MbCountersServerStatistics( [Key] Guid ServerId,
[Increment<MbCountersUserConnected>] [Decrement<MbCountersUserDisconnected>] int ActiveConnections);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.CountersUserConnected do use Chronicle.Events.EventType, id: "counters-user-connected-v1"
defstruct []end
defmodule MyApp.Events.CountersUserDisconnected do use Chronicle.Events.EventType, id: "counters-user-disconnected-v1"
defstruct []end
defmodule MyApp.ReadModels.CountersServerStatistics do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{CountersUserConnected, CountersUserDisconnected}
defstruct [:server_id, active_connections: 0]
from CountersUserConnected, set: [server_id: :event_source_id], add: [active_connections: 1]
from CountersUserDisconnected, subtract: [active_connections: 1]endimport { eventType } from '@cratis/chronicle';
@eventType()class MbCountersUserConnected {}
@eventType()class MbCountersUserDisconnected {}
@readModel()class MbCountersServerStatistics { id: Guid = Guid.empty;
@increment(MbCountersUserConnected) @decrement(MbCountersUserDisconnected) activeConnections = 0;}When a UserConnected event occurs, ActiveConnections increases by 1. When a UserDisconnected event occurs, it decreases by 1.
The Count attribute counts the total number of times an event occurs. Unlike Increment, Count doesn’t increment from a current value—it maintains an absolute count.
[EventType]public record MbCountersOrderPlaced;
[EventType]public record MbCountersOrderCancelled;
public record MbCountersEventMetrics( [Key] Guid Id,
[Count<MbCountersOrderPlaced>] int TotalOrders,
[Count<MbCountersOrderCancelled>] int CancelledOrders);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.CountersOrderPlaced do use Chronicle.Events.EventType, id: "counters-order-placed-v1"
defstruct []end
defmodule MyApp.Events.CountersOrderCancelled do use Chronicle.Events.EventType, id: "counters-order-cancelled-v1"
defstruct []end
defmodule MyApp.ReadModels.CountersEventMetrics do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{CountersOrderPlaced, CountersOrderCancelled}
defstruct [:id, total_orders: 0, cancelled_orders: 0]
from CountersOrderPlaced, set: [id: :event_source_id], count: :total_orders
from CountersOrderCancelled, count: :cancelled_ordersendimport { eventType } from '@cratis/chronicle';
@eventType()class MbCountersOrderPlaced {}
@eventType()class MbCountersOrderCancelled {}
@readModel()class MbCountersEventMetrics { id: Guid = Guid.empty;
@count(MbCountersOrderPlaced) totalOrders = 0;
@count(MbCountersOrderCancelled) cancelledOrders = 0;}Multiple Events
Section titled “Multiple Events”You can use multiple attributes on the same property to respond to different events:
[EventType]public record MbCountersItemCreated(string Name, int InitialQuantity);
[EventType]public record MbCountersItemRestocked;
[EventType]public record MbCountersItemSold;
public record MbCountersInventoryItem( [Key] Guid ItemId,
[SetFrom<MbCountersItemCreated>(nameof(MbCountersItemCreated.Name))] string Name,
[SetFrom<MbCountersItemCreated>(nameof(MbCountersItemCreated.InitialQuantity))] [Increment<MbCountersItemRestocked>] [Decrement<MbCountersItemSold>] int Quantity,
[Count<MbCountersItemRestocked>] int RestockCount,
[Count<MbCountersItemSold>] int SalesCount);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.CountersItemCreated do use Chronicle.Events.EventType, id: "counters-item-created-v1"
defstruct [:name, :initial_quantity]end
defmodule MyApp.Events.CountersItemRestocked do use Chronicle.Events.EventType, id: "counters-item-restocked-v1"
defstruct []end
defmodule MyApp.Events.CountersItemSold do use Chronicle.Events.EventType, id: "counters-item-sold-v1"
defstruct []end
defmodule MyApp.ReadModels.CountersInventoryItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{CountersItemCreated, CountersItemRestocked, CountersItemSold}
defstruct [:item_id, :name, quantity: 0, restock_count: 0, sales_count: 0]
from CountersItemCreated, set: [item_id: :event_source_id, name: :name, quantity: :initial_quantity]
from CountersItemRestocked, add: [quantity: 1], count: :restock_count
from CountersItemSold, subtract: [quantity: 1], count: :sales_countendimport { eventType } from '@cratis/chronicle';
@eventType()class MbCountersItemCreated { name = ''; initialQuantity = 0;}
@eventType()class MbCountersItemRestocked {}
@eventType()class MbCountersItemSold {}
@readModel()class MbCountersInventoryItem { id: Guid = Guid.empty;
@setFrom(MbCountersItemCreated, 'name') name = '';
@setFrom(MbCountersItemCreated, 'initialQuantity') @increment(MbCountersItemRestocked) @decrement(MbCountersItemSold) quantity = 0;
@count(MbCountersItemRestocked) restockCount = 0;
@count(MbCountersItemSold) salesCount = 0;}Complete Example
Section titled “Complete Example”Here’s a complete example tracking various metrics:
// Events[EventType]public record MbCountersUserLoggedInFull(DateTimeOffset Timestamp);
[EventType]public record MbCountersUserLoggedOutFull(DateTimeOffset Timestamp);
[EventType]public record MbCountersPurchaseMade(decimal Amount);
[EventType]public record MbCountersRefundIssued(decimal Amount);
// Read Modelpublic record MbCountersUserActivity( [Key] Guid UserId,
// Track login/logout counts [Count<MbCountersUserLoggedInFull>] int TotalLogins,
[Count<MbCountersUserLoggedOutFull>] int TotalLogouts,
// Track active sessions [Increment<MbCountersUserLoggedInFull>] [Decrement<MbCountersUserLoggedOutFull>] int ActiveSessions,
// Track transaction counts [Count<MbCountersPurchaseMade>] int PurchaseCount,
[Count<MbCountersRefundIssued>] int RefundCount,
// Track transaction values [AddFrom<MbCountersPurchaseMade>(nameof(MbCountersPurchaseMade.Amount))] [SubtractFrom<MbCountersRefundIssued>(nameof(MbCountersRefundIssued.Amount))] decimal NetSpent);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.CountersUserLoggedInFull do use Chronicle.Events.EventType, id: "counters-user-logged-in-full-v1"
defstruct [:timestamp]end
defmodule MyApp.Events.CountersUserLoggedOutFull do use Chronicle.Events.EventType, id: "counters-user-logged-out-full-v1"
defstruct [:timestamp]end
defmodule MyApp.Events.CountersPurchaseMade do use Chronicle.Events.EventType, id: "counters-purchase-made-v1"
defstruct [:amount]end
defmodule MyApp.Events.CountersRefundIssued do use Chronicle.Events.EventType, id: "counters-refund-issued-v1"
defstruct [:amount]end
defmodule MyApp.ReadModels.CountersUserActivity do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ CountersUserLoggedInFull, CountersUserLoggedOutFull, CountersPurchaseMade, CountersRefundIssued }
defstruct [ :user_id, total_logins: 0, total_logouts: 0, active_sessions: 0, purchase_count: 0, refund_count: 0, net_spent: 0 ]
# Track login/logout counts and active sessions from CountersUserLoggedInFull, set: [user_id: :event_source_id], count: :total_logins, add: [active_sessions: 1]
from CountersUserLoggedOutFull, count: :total_logouts, subtract: [active_sessions: 1]
# Track transaction counts and net value from CountersPurchaseMade, count: :purchase_count, add: [net_spent: :amount]
from CountersRefundIssued, count: :refund_count, subtract: [net_spent: :amount]endimport { addFrom, count, decrement, eventType, Guid, increment, readModel, subtractFrom } from '@cratis/chronicle';
// Events@eventType()class MbCountersUserLoggedInFull { timestamp = new Date();}
@eventType()class MbCountersUserLoggedOutFull { timestamp = new Date();}
@eventType()class MbCountersPurchaseMade { amount = 0;}
@eventType()class MbCountersRefundIssued { amount = 0;}
// Read Model@readModel()class MbCountersUserActivity { id: Guid = Guid.empty;
// Track login/logout counts @count(MbCountersUserLoggedInFull) totalLogins = 0;
@count(MbCountersUserLoggedOutFull) totalLogouts = 0;
// Track active sessions @increment(MbCountersUserLoggedInFull) @decrement(MbCountersUserLoggedOutFull) activeSessions = 0;
// Track transaction counts @count(MbCountersPurchaseMade) purchaseCount = 0;
@count(MbCountersRefundIssued) refundCount = 0;
// Track transaction values @addFrom(MbCountersPurchaseMade, 'amount') @subtractFrom(MbCountersRefundIssued, 'amount') netSpent = 0;}Counter vs Count
Section titled “Counter vs Count”Increment/Decrement:
- Modifies the current value
- Useful for tracking active states (sessions, connections)
- Can be combined with SetFrom to establish initial values
- Changes are relative to current value
Count:
- Maintains absolute count of event occurrences
- Useful for analytics and reporting
- Independent of other operations
- Always represents total occurrences
Best Practices
Section titled “Best Practices”- Use Increment/Decrement for tracking active/current states that change over time
- Use Count for analytics and metrics that track total occurrences
- Combine operations on the same property when tracking both current state and history
- Initialize counters with SetFrom when you have an initial value from a creation event