From 4470ef52b0e61188e5d430020520c044b7adf048 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 12 Aug 2026 18:52:58 +0200 Subject: [PATCH 1/2] Add stream reading guidance: ReadStreamToEnd, memory semantics Document the IEventReader read semantics (KurrentDB streams events as they arrive, relational stores buffer up to count) and steer agents to ReadStreamToEnd for whole-stream reads instead of ReadEvents with int.MaxValue. Matches Eventuous/eventuous#568. Co-Authored-By: Claude Fable 5 --- agents/eventuous-expert.md | 1 + skills/eventuous-dotnet-kurrentdb/SKILL.md | 2 ++ skills/eventuous-dotnet/SKILL.md | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/agents/eventuous-expert.md b/agents/eventuous-expert.md index 4bb4920..de117cd 100644 --- a/agents/eventuous-expert.md +++ b/agents/eventuous-expert.md @@ -62,6 +62,7 @@ Determine the target language from the project context: - Recommend KurrentDB as the default event store unless the user specifies otherwise - Prefer functional command services (`CommandService`) for simple cases; aggregate-based (`CommandService`) when business invariants require it - Use `IEventReader.LoadAggregate<>()` and `IEventWriter.StoreAggregate<>()` extension methods — `IAggregateStore` is deprecated +- Read whole streams with `IEventReader.ReadStreamToEnd()` (paged, bounded memory) — never `ReadEvents` with `int.MaxValue` as the count - Use `.NoContext()` for all async calls (`ConfigureAwait(false)`) - Event types are registered automatically via source generation (no manual `TypeMap` calls) - Follow the default stream naming convention: `{AggregateType}-{AggregateId}` diff --git a/skills/eventuous-dotnet-kurrentdb/SKILL.md b/skills/eventuous-dotnet-kurrentdb/SKILL.md index ba9a5c4..2688155 100644 --- a/skills/eventuous-dotnet-kurrentdb/SKILL.md +++ b/skills/eventuous-dotnet-kurrentdb/SKILL.md @@ -50,6 +50,8 @@ services.AddEventStore(); `KurrentDBEventStore` implements `IEventStore` (which combines `IEventReader` and `IEventWriter`). `AddEventStore()` registers all three interfaces, with tracing wrappers when diagnostics are enabled. +Reads stream events as they arrive from the server — a read holds at most one deserialized event at a time regardless of the requested count. To read a whole stream, use the `ReadStreamToEnd` extension method instead of `ReadEvents` with `int.MaxValue`. + The legacy class `EsdbEventStore` is obsolete -- use `KurrentDBEventStore` instead. ## Subscriptions diff --git a/skills/eventuous-dotnet/SKILL.md b/skills/eventuous-dotnet/SKILL.md index f86d921..ee01620 100644 --- a/skills/eventuous-dotnet/SKILL.md +++ b/skills/eventuous-dotnet/SKILL.md @@ -250,6 +250,22 @@ Extracting ID from stream name (useful in projections): `ctx.Stream.GetId()`. --- +## Reading Event Streams + +`IEventReader.ReadEvents`/`ReadEventsBackwards` return `IAsyncEnumerable` and read a fixed number of events. `KurrentDBEventStore` streams events as they arrive (holds one deserialized event at a time); relational stores buffer up to `count` events per call, so keep the count bounded. + +To read a whole stream, use the `ReadStreamToEnd` extension method — never `ReadEvents` with `int.MaxValue` as the count: + +```csharp +await foreach (var evt in eventReader.ReadStreamToEnd(streamName, StreamReadPosition.Start, cancellationToken: ct)) { + // One event at a time, memory bounded by page size (default 500) +} +``` + +Options: `pageSize` tunes the page size; `failIfNotFound: false` yields nothing instead of throwing `StreamNotFound`. The `ReadStream` extension method does the same paged read and returns `StreamEvent[]` if you need the whole stream as an array. + +--- + ## HTTP API ### Controller-Based From 770beceaf6ebbc7dc8644d9154f3d045f08f7b37 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 19 Aug 2026 17:35:40 +0200 Subject: [PATCH 2/2] Add reader contract details from the review-hardened streaming changes State the enforced read contract (exact count unless stream end, past-end reads return empty, missing stream throws), the KurrentDB system-event compensation, and the pageSize validation. Matches Eventuous/eventuous#568. Co-Authored-By: Claude Fable 5 --- skills/eventuous-dotnet-kurrentdb/SKILL.md | 2 +- skills/eventuous-dotnet/SKILL.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/skills/eventuous-dotnet-kurrentdb/SKILL.md b/skills/eventuous-dotnet-kurrentdb/SKILL.md index 2688155..4ee678d 100644 --- a/skills/eventuous-dotnet-kurrentdb/SKILL.md +++ b/skills/eventuous-dotnet-kurrentdb/SKILL.md @@ -50,7 +50,7 @@ services.AddEventStore(); `KurrentDBEventStore` implements `IEventStore` (which combines `IEventReader` and `IEventWriter`). `AddEventStore()` registers all three interfaces, with tracing wrappers when diagnostics are enabled. -Reads stream events as they arrive from the server — a read holds at most one deserialized event at a time regardless of the requested count. To read a whole stream, use the `ReadStreamToEnd` extension method instead of `ReadEvents` with `int.MaxValue`. +Reads stream events as they arrive from the server — a read holds at most one deserialized event at a time regardless of the requested count. Non-deserializable `$`-prefixed system events are skipped, with follow-up reads compensating so the requested count is still delivered. To read a whole stream, use the `ReadStreamToEnd` extension method instead of `ReadEvents` with `int.MaxValue`. The legacy class `EsdbEventStore` is obsolete -- use `KurrentDBEventStore` instead. diff --git a/skills/eventuous-dotnet/SKILL.md b/skills/eventuous-dotnet/SKILL.md index ee01620..6bdb496 100644 --- a/skills/eventuous-dotnet/SKILL.md +++ b/skills/eventuous-dotnet/SKILL.md @@ -252,7 +252,9 @@ Extracting ID from stream name (useful in projections): `ctx.Stream.GetId()`. ## Reading Event Streams -`IEventReader.ReadEvents`/`ReadEventsBackwards` return `IAsyncEnumerable` and read a fixed number of events. `KurrentDBEventStore` streams events as they arrive (holds one deserialized event at a time); relational stores buffer up to `count` events per call, so keep the count bounded. +`IEventReader.ReadEvents`/`ReadEventsBackwards` return `IAsyncEnumerable` and read a fixed number of events. `KurrentDBEventStore` streams events as they arrive (holds one deserialized event at a time); relational stores buffer events proportional to `count` per call, so keep the count bounded. + +Reader contract (all stores): a read yields exactly `count` events unless the stream end is reached, so a short read means end of stream; reading past the end of an existing stream returns an empty sequence, and only a missing stream throws `StreamNotFound`. To read a whole stream, use the `ReadStreamToEnd` extension method — never `ReadEvents` with `int.MaxValue` as the count: @@ -262,7 +264,7 @@ await foreach (var evt in eventReader.ReadStreamToEnd(streamName, StreamReadPosi } ``` -Options: `pageSize` tunes the page size; `failIfNotFound: false` yields nothing instead of throwing `StreamNotFound`. The `ReadStream` extension method does the same paged read and returns `StreamEvent[]` if you need the whole stream as an array. +Options: `pageSize` tunes the page size (must be positive, throws `ArgumentOutOfRangeException` otherwise); `failIfNotFound: false` yields nothing instead of throwing `StreamNotFound`. The `ReadStream` extension method does the same paged read and returns `StreamEvent[]` if you need the whole stream as an array. ---