Events
Events are immutable records of things that happened in your system. They’re stored permanently in per-aggregate streams with monotonically increasing version numbers. Events are the source of truth — everything else (aggregate state, projections, read models) is derived from them.
Appending Events
Section titled “Appending Events”Single Event
Section titled “Single Event”var result = await hapnd.Aggregate("order_123") .Append(new OrderPlaced { CustomerId = "cust_456" });
Console.WriteLine($"Event {result.EventId} at version {result.Version}");TypeScript
Section titled “TypeScript”const result = await hapnd .aggregate("order_123") .append("OrderPlaced", { customerId: "cust_456" });
console.log(`Event ${result.eventId} at version ${result.version}`);Batch Append
Section titled “Batch Append”Append multiple events atomically — all succeed or none do. This is essential for maintaining consistency when a single action produces multiple events.
var result = await hapnd.Aggregate("order_123") .AppendMany([ new ItemAdded { Item = "Widget", Price = 25.00m }, new ItemAdded { Item = "Gadget", Price = 15.00m } ]);
Console.WriteLine($"Versions {result.StartVersion} to {result.EndVersion}");TypeScript
Section titled “TypeScript”const result = await hapnd.aggregate("order_123").appendMany([ { eventType: "ItemAdded", data: { item: "Widget", price: 25.0 } }, { eventType: "ItemAdded", data: { item: "Gadget", price: 15.0 } },]);
console.log(`Versions ${result.startVersion} to ${result.endVersion}`);Event Types
Section titled “Event Types”The event type is a string discriminator stored with each event. Your reducers and projections use it to determine how to process the event.
.NET — Automatic Resolution
Section titled “.NET — Automatic Resolution”The SDK resolves the event type from the class name automatically:
// Event type: "OrderPlaced"await hapnd.Aggregate("order_123").Append(new OrderPlaced { ... });
// Event type: "ItemAdded"await hapnd.Aggregate("order_123").Append(new ItemAdded { ... });Override with the [EventType] attribute when the class name doesn’t match your domain language:
using Hapnd.Client;
[EventType("item_added")]public class AddItemToOrder{ public string Item { get; init; } public decimal Price { get; init; }}
// Event type: "item_added"await hapnd.Aggregate("order_123").Append(new AddItemToOrder { ... });TypeScript — Explicit
Section titled “TypeScript — Explicit”In TypeScript, the event type is always passed explicitly as the first argument:
await hapnd .aggregate("order_123") .append("ItemAdded", { item: "Widget", price: 25.0 });Optimistic Concurrency
Section titled “Optimistic Concurrency”Use ExpectVersion to prevent lost updates when multiple processes write to the same aggregate concurrently. The server rejects the append if the aggregate’s current version doesn’t match.
try{ await hapnd.Aggregate("order_123") .ExpectVersion(5) .Append(new ItemAdded { Item = "Widget", Price = 25.00m });}catch (HapndConcurrencyException ex){ Console.WriteLine($"Expected version {ex.ExpectedVersion}, actual {ex.ActualVersion}"); // Reload state and retry with business logic}TypeScript
Section titled “TypeScript”import { HapndConcurrencyError } from "@hapnd/client";
try { await hapnd .aggregate("order_123") .expectVersion(5) .append("ItemAdded", { item: "Widget", price: 25.0 });} catch (err) { if (err instanceof HapndConcurrencyError) { console.log(`Expected ${err.expectedVersion}, actual ${err.actualVersion}`); // Reload state and retry with business logic }}Use ExpectVersion(0) (or expectVersion(0)) to assert the aggregate doesn’t exist yet — useful for creation commands.
Distributed Tracing
Section titled “Distributed Tracing”Correlation IDs
Section titled “Correlation IDs”Group related operations across services. Typically propagated from an incoming HTTP request:
await hapnd.Aggregate("order_123") .WithCorrelation(correlationId) .Append(new ItemAdded { Item = "Widget" });TypeScript
Section titled “TypeScript”await hapnd .aggregate("order_123") .withCorrelation(correlationId) .append("ItemAdded", { item: "Widget" });Causation IDs
Section titled “Causation IDs”Link events in a cause-and-effect chain. Set to the EventId of the event that triggered this operation:
var first = await hapnd.Aggregate("order_123") .Append(new OrderPlaced { CustomerId = "cust_456" });
await hapnd.Aggregate("order_123") .WithCausation(first.EventId) .Append(new PaymentRequested { Amount = 50.00m });TypeScript
Section titled “TypeScript”const first = await hapnd .aggregate("order_123") .append("OrderPlaced", { customerId: "cust_456" });
await hapnd .aggregate("order_123") .withCausation(first.eventId) .append("PaymentRequested", { amount: 50.0 });Metadata
Section titled “Metadata”Attach arbitrary context to events. Stored alongside the event but not part of the event data itself:
await hapnd.Aggregate("order_123") .WithMetadata(new { UserId = "user_456", IpAddress = "192.168.1.1", UserAgent = "MyApp/1.0" }) .Append(new OrderPlaced { CustomerId = "cust_456" });TypeScript
Section titled “TypeScript”await hapnd .aggregate("order_123") .withMetadata({ userId: "user_456", ipAddress: "192.168.1.1", userAgent: "MyApp/1.0", }) .append("OrderPlaced", { customerId: "cust_456" });