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);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Incrementimport io.cratis.chronicle.readModels.ReadModel
@EventTypeclass MbCountersUserLoggedIn
@ReadModel@FromEvent(MbCountersUserLoggedIn::class)data class MbCountersUserStatistics( @Increment(MbCountersUserLoggedIn::class) val loginCount: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Increment;import io.cratis.chronicle.readModels.ReadModel;
@EventTypeclass MbCountersUserLoggedIn {}
@ReadModel@FromEvent(eventType = MbCountersUserLoggedIn.class)class MbCountersUserStatistics { @Increment(eventType = MbCountersUserLoggedIn.class) public int loginCount = 0;}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 } from '@cratis/chronicle';
@eventType()export class MbCountersUserLoggedIn {}
export 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);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.Decrementimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Incrementimport io.cratis.chronicle.readModels.ReadModel
@EventTypeclass MbCountersUserConnected
@EventTypeclass MbCountersUserDisconnected
@ReadModel@FromEvent(MbCountersUserConnected::class)@FromEvent(MbCountersUserDisconnected::class)data class MbCountersServerStatistics( @Increment(MbCountersUserConnected::class) @Decrement(MbCountersUserDisconnected::class) val activeConnections: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.Decrement;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Increment;import io.cratis.chronicle.readModels.ReadModel;
@EventTypeclass MbCountersUserConnected {}
@EventTypeclass MbCountersUserDisconnected {}
@ReadModel@FromEvent(eventType = MbCountersUserConnected.class)@FromEvent(eventType = MbCountersUserDisconnected.class)class MbCountersServerStatistics { @Increment(eventType = MbCountersUserConnected.class) @Decrement(eventType = MbCountersUserDisconnected.class) public int activeConnections = 0;}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()export class MbCountersUserConnected {}
@eventType()export class MbCountersUserDisconnected {}
export 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 adds one to the property for every matching event, starting from zero, so on its own it holds the number of times the event occurred. It is not a separate running tally: it adds to whatever the property currently holds, so another attribute that writes the same property changes the number it counts on from.
[EventType]public record MbCountersOrderPlaced;
[EventType]public record MbCountersOrderCancelled;
public record MbCountersEventMetrics( [Key] Guid Id,
[Count<MbCountersOrderPlaced>] int TotalOrders,
[Count<MbCountersOrderCancelled>] int CancelledOrders);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.Countimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventTypeclass MbCountersOrderPlaced
@EventTypeclass MbCountersOrderCancelled
@ReadModel@FromEvent(MbCountersOrderPlaced::class)@FromEvent(MbCountersOrderCancelled::class)data class MbCountersEventMetrics( @Count(MbCountersOrderPlaced::class) val totalOrders: Int = 0,
@Count(MbCountersOrderCancelled::class) val cancelledOrders: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.Count;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventTypeclass MbCountersOrderPlaced {}
@EventTypeclass MbCountersOrderCancelled {}
@ReadModel@FromEvent(eventType = MbCountersOrderPlaced.class)@FromEvent(eventType = MbCountersOrderCancelled.class)class MbCountersEventMetrics { @Count(eventType = MbCountersOrderPlaced.class) public int totalOrders = 0;
@Count(eventType = MbCountersOrderCancelled.class) public int cancelledOrders = 0;}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()export class MbCountersOrderPlaced {}
@eventType()export class MbCountersOrderCancelled {}
export 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);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.Countimport io.cratis.chronicle.projections.Decrementimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Incrementimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbCountersItemCreated(val name: String, val initialQuantity: Int)
@EventTypeclass MbCountersItemRestocked
@EventTypeclass MbCountersItemSold
@ReadModel@FromEvent(MbCountersItemCreated::class)@FromEvent(MbCountersItemRestocked::class)@FromEvent(MbCountersItemSold::class)data class MbCountersInventoryItem( @SetFrom("name", MbCountersItemCreated::class) val name: String = "",
@SetFrom("initialQuantity", MbCountersItemCreated::class) @Increment(MbCountersItemRestocked::class) @Decrement(MbCountersItemSold::class) val quantity: Int = 0,
@Count(MbCountersItemRestocked::class) val restockCount: Int = 0,
@Count(MbCountersItemSold::class) val salesCount: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.Count;import io.cratis.chronicle.projections.Decrement;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Increment;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbCountersItemCreated(String name, int initialQuantity) {}
@EventTypeclass MbCountersItemRestocked {}
@EventTypeclass MbCountersItemSold {}
@ReadModel@FromEvent(eventType = MbCountersItemCreated.class)@FromEvent(eventType = MbCountersItemRestocked.class)@FromEvent(eventType = MbCountersItemSold.class)class MbCountersInventoryItem { @SetFrom(propertyPath = "name", eventType = MbCountersItemCreated.class) public String name = "";
@SetFrom(propertyPath = "initialQuantity", eventType = MbCountersItemCreated.class) @Increment(eventType = MbCountersItemRestocked.class) @Decrement(eventType = MbCountersItemSold.class) public int quantity = 0;
@Count(eventType = MbCountersItemRestocked.class) public int restockCount = 0;
@Count(eventType = MbCountersItemSold.class) public int salesCount = 0;}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()export class MbCountersItemCreated { name = ''; initialQuantity = 0;}
@eventType()export class MbCountersItemRestocked {}
@eventType()export class MbCountersItemSold {}
export 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);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.AddFromimport io.cratis.chronicle.projections.Countimport io.cratis.chronicle.projections.Decrementimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Incrementimport io.cratis.chronicle.projections.SubtractFromimport io.cratis.chronicle.readModels.ReadModelimport java.time.Instant
// Events@EventTypedata class MbCountersUserLoggedInFull(val timestamp: Instant)
@EventTypedata class MbCountersUserLoggedOutFull(val timestamp: Instant)
@EventTypedata class MbCountersPurchaseMade(val amount: Double)
@EventTypedata class MbCountersRefundIssued(val amount: Double)
// Read Model@ReadModel@FromEvent(MbCountersUserLoggedInFull::class)@FromEvent(MbCountersUserLoggedOutFull::class)@FromEvent(MbCountersPurchaseMade::class)@FromEvent(MbCountersRefundIssued::class)data class MbCountersUserActivity( // Track login/logout counts @Count(MbCountersUserLoggedInFull::class) val totalLogins: Int = 0,
@Count(MbCountersUserLoggedOutFull::class) val totalLogouts: Int = 0,
// Track active sessions @Increment(MbCountersUserLoggedInFull::class) @Decrement(MbCountersUserLoggedOutFull::class) val activeSessions: Int = 0,
// Track transaction counts @Count(MbCountersPurchaseMade::class) val purchaseCount: Int = 0,
@Count(MbCountersRefundIssued::class) val refundCount: Int = 0,
// Track transaction values @AddFrom(MbCountersPurchaseMade::class, "amount") @SubtractFrom(MbCountersRefundIssued::class, "amount") val netSpent: Double = 0.0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.AddFrom;import io.cratis.chronicle.projections.Count;import io.cratis.chronicle.projections.Decrement;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Increment;import io.cratis.chronicle.projections.SubtractFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.time.Instant;
// Events@EventTyperecord MbCountersUserLoggedInFull(Instant timestamp) {}
@EventTyperecord MbCountersUserLoggedOutFull(Instant timestamp) {}
@EventTyperecord MbCountersPurchaseMade(double amount) {}
@EventTyperecord MbCountersRefundIssued(double amount) {}
// Read Model@ReadModel@FromEvent(eventType = MbCountersUserLoggedInFull.class)@FromEvent(eventType = MbCountersUserLoggedOutFull.class)@FromEvent(eventType = MbCountersPurchaseMade.class)@FromEvent(eventType = MbCountersRefundIssued.class)class MbCountersUserActivity { // Track login/logout counts @Count(eventType = MbCountersUserLoggedInFull.class) public int totalLogins = 0;
@Count(eventType = MbCountersUserLoggedOutFull.class) public int totalLogouts = 0;
// Track active sessions @Increment(eventType = MbCountersUserLoggedInFull.class) @Decrement(eventType = MbCountersUserLoggedOutFull.class) public int activeSessions = 0;
// Track transaction counts @Count(eventType = MbCountersPurchaseMade.class) public int purchaseCount = 0;
@Count(eventType = MbCountersRefundIssued.class) public int refundCount = 0;
// Track transaction values @AddFrom(eventType = MbCountersPurchaseMade.class, eventPropertyName = "amount") @SubtractFrom(eventType = MbCountersRefundIssued.class, eventPropertyName = "amount") public double netSpent = 0.0;}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, subtractFrom } from '@cratis/chronicle';
// Events@eventType()export class MbCountersUserLoggedInFull { timestamp = new Date();}
@eventType()export class MbCountersUserLoggedOutFull { timestamp = new Date();}
@eventType()export class MbCountersPurchaseMade { amount = 0;}
@eventType()export class MbCountersRefundIssued { amount = 0;}
// Read Modelexport 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