diff --git a/src/Core/src/Eventuous.Persistence/EventStore/IEventReader.cs b/src/Core/src/Eventuous.Persistence/EventStore/IEventReader.cs
index 8268287b9..72290ed2c 100644
--- a/src/Core/src/Eventuous.Persistence/EventStore/IEventReader.cs
+++ b/src/Core/src/Eventuous.Persistence/EventStore/IEventReader.cs
@@ -7,6 +7,10 @@ public interface IEventReader {
///
/// Read a fixed number of events from an existing stream as an async enumerable.
/// Throws if the stream does not exist.
+ /// Implementations either stream events as they arrive from the store, or buffer up to
+ /// events before yielding, so memory usage can grow with . To read a whole stream,
+ /// use , which reads in pages, instead of passing
+ /// as the count.
///
/// Stream name
/// Where to start reading events
@@ -18,6 +22,8 @@ public interface IEventReader {
///
/// Read a number of events from a given stream, backwards (from the stream end).
/// Throws if the stream does not exist.
+ /// Implementations either stream events as they arrive from the store, or buffer up to
+ /// events before yielding, so memory usage can grow with .
///
/// Stream name
/// Where to start reading events
diff --git a/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs b/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs
index ca2ee79f2..7a09b6b3d 100644
--- a/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs
+++ b/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs
@@ -1,6 +1,8 @@
// Copyright (C) Eventuous HQ OÜ. All rights reserved
// Licensed under the Apache License, Version 2.0.
+using System.Runtime.CompilerServices;
+
namespace Eventuous;
public static class StoreFunctions {
@@ -148,6 +150,59 @@ CancellationToken cancellationToken
}
}
+ ///
+ /// Reads a stream from the given position to the end, as an async enumerable.
+ /// Events are read in pages of and yielded as they arrive, so the whole stream
+ /// is never buffered in memory. Use this instead of calling
+ /// with as the count.
+ ///
+ /// Name of the stream to read from
+ /// Stream position to start reading from
+ /// Number of events to read per page. It caps the amount of events a buffering
+ /// implementation of holds in memory at a time.
+ /// Set to false to complete without yielding anything when the stream isn't found,
+ /// instead of throwing . Default is true.
+ /// Cancellation token
+ /// An async enumerable of events retrieved from the stream
+ public async IAsyncEnumerable ReadStreamToEnd(
+ StreamName streamName,
+ StreamReadPosition start,
+ int pageSize = 500,
+ bool failIfNotFound = true,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ ) {
+ var position = start;
+
+ while (true) {
+ var yielded = 0;
+ long lastRevision = 0;
+
+ await using var enumerator = eventReader.ReadEvents(streamName, position, pageSize, cancellationToken).GetAsyncEnumerator(cancellationToken);
+
+ while (true) {
+ bool moved;
+
+ try {
+ moved = await enumerator.MoveNextAsync().NoContext();
+ } catch (StreamNotFound) when (!failIfNotFound) {
+ yield break;
+ }
+
+ if (!moved) break;
+
+ var evt = enumerator.Current;
+ yielded++;
+ lastRevision = evt.Revision;
+
+ yield return evt;
+ }
+
+ if (yielded < pageSize) yield break;
+
+ position = new(lastRevision + 1);
+ }
+ }
+
///
/// Reads a stream from the event store to a collection of
///
@@ -163,23 +218,10 @@ public async Task ReadStream(
bool failIfNotFound = true,
CancellationToken cancellationToken = default
) {
- const int pageSize = 500;
-
var streamEvents = new List();
- var position = start;
-
- try {
- while (true) {
- var events = await eventReader.ReadEvents(streamName, position, pageSize, failIfNotFound, cancellationToken).NoContext();
- streamEvents.AddRange(events);
-
- if (events.Length < pageSize) break;
-
- position = new(position.Value + events.Length);
- }
- } catch (StreamNotFound) when (!failIfNotFound) {
- return [];
+ await foreach (var evt in eventReader.ReadStreamToEnd(streamName, start, failIfNotFound: failIfNotFound, cancellationToken: cancellationToken).NoContext(cancellationToken)) {
+ streamEvents.Add(evt);
}
return [.. streamEvents];
diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs
index 25adbbfba..d8f7b0c62 100644
--- a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs
+++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs
@@ -146,6 +146,102 @@ public async Task ShouldReturnWhenReadingBackwards(CancellationToken cancellatio
await Assert.That(result.Length).IsEqualTo(5);
}
+ [Test]
+ [Category("Store")]
+ public async Task ShouldThrowWhenReadingMissingStream(CancellationToken cancellationToken) {
+ var streamName = Helpers.GetStreamName();
+
+ await Assert.ThrowsAsync(() => _fixture.EventStore.ReadEvents(streamName, StreamReadPosition.Start, 10, true, cancellationToken));
+ }
+
+ [Test]
+ [Category("Store")]
+ public async Task ShouldThrowWhenReadingMissingStreamBackwards(CancellationToken cancellationToken) {
+ var streamName = Helpers.GetStreamName();
+
+ await Assert.ThrowsAsync(() => _fixture.EventStore.ReadEventsBackwards(streamName, StreamReadPosition.End, 10, true, cancellationToken));
+ }
+
+ [Test]
+ [Category("Store")]
+ public async Task ShouldReadStreamToEnd(CancellationToken cancellationToken) {
+ object[] events = [.. _fixture.CreateEvents(25)];
+ var streamName = Helpers.GetStreamName();
+ await _fixture.AppendEvents(streamName, events, ExpectedStreamVersion.NoStream);
+
+ var result = new List();
+
+ await foreach (var evt in _fixture.EventStore.ReadStreamToEnd(streamName, StreamReadPosition.Start, pageSize: 10, cancellationToken: cancellationToken)) {
+ result.Add(evt);
+ }
+
+ IEnumerable