From 5bd189a5554a981f4284698c5db7b73db366eb35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?=
<495495+thefringeninja@users.noreply.github.com>
Date: Wed, 28 Feb 2024 01:36:29 -0800
Subject: [PATCH] Subscriptions Without Callbacks (DEV-112) (#282)
* add sensible subscriptions
* add sensible persistent subscriptions
* obsolete old subscription / ps apis
* put missing props on sever exception in net48
---
Directory.Build.props | 4 +-
samples/persistent-subscriptions/Program.cs | 139 ++--
samples/server-side-filtering/Program.cs | 178 +++--
samples/subscribing-to-streams/Program.cs | 288 ++++---
.../AsyncStreamReaderExtensions.cs | 16 +-
.../protos/streams.proto | 13 +-
...StorePersistentSubscriptionsClient.Read.cs | 361 ++++++++-
...EventStorePersistentSubscriptionsClient.cs | 10 +-
.../PersistentSubscription.cs | 188 ++---
.../PersistentSubscriptionMessage.cs | 33 +
.../EventStore.Client.Streams.csproj | 3 -
.../EventStoreClient.Append.cs | 15 +-
.../EventStoreClient.Read.cs | 71 +-
.../EventStoreClient.Subscriptions.cs | 233 +++++-
.../EventStoreClient.cs | 12 +-
.../StreamMessage.cs | 32 +
.../StreamSubscription.cs | 160 ++--
.../Streams/BatchAppendReq.cs | 4 +-
.../SubscriptionConfirmation.cs | 24 -
.../SubscriptionFilterOptions.cs | 24 +-
.../EventStore.Client.csproj | 6 +-
.../Bugs/Issue_1125.cs | 77 +-
...connect_to_existing_with_max_one_client.cs | 35 +-
...o_existing_with_max_one_client_obsolete.cs | 43 ++
.../connect_to_existing_with_permissions.cs | 31 +-
...t_to_existing_with_permissions_obsolete.cs | 37 +
...t_to_existing_with_start_from_beginning.cs | 75 +-
...ting_with_start_from_beginning_obsolete.cs | 66 ++
...ect_to_existing_with_start_from_not_set.cs | 76 +-
...isting_with_start_from_not_set_obsolete.cs | 64 ++
...h_start_from_not_set_then_event_written.cs | 73 +-
...rom_not_set_then_event_written_obsolete.cs | 72 ++
...ing_with_start_from_set_to_end_position.cs | 76 +-
...start_from_set_to_end_position_obsolete.cs | 63 ++
..._set_to_end_position_then_event_written.cs | 72 +-
...nd_position_then_event_written_obsolete.cs | 69 ++
...art_from_set_to_invalid_middle_position.cs | 58 +-
...set_to_invalid_middle_position_obsolete.cs | 49 ++
...start_from_set_to_valid_middle_position.cs | 63 +-
...m_set_to_valid_middle_position_obsolete.cs | 65 ++
...connect_to_existing_without_permissions.cs | 22 +-
...o_existing_without_permissions_obsolete.cs | 32 +
...o_existing_without_read_all_permissions.cs | 23 +-
...g_without_read_all_permissions_obsolete.cs | 33 +
...onnect_to_non_existing_with_permissions.cs | 23 +-
..._non_existing_with_permissions_obsolete.cs | 32 +
.../SubscriptionToAll/connect_with_retries.cs | 75 +-
.../connect_with_retries_obsolete.cs | 59 ++
.../deleting_existing_with_subscriber.cs | 66 +-
...eting_existing_with_subscriber_obsolete.cs | 66 ++
.../SubscriptionToAll/get_info.cs | 109 ++-
.../SubscriptionToAll/get_info_obsolete.cs | 183 +++++
...atching_up_to_link_to_events_manual_ack.cs | 78 +-
...p_to_link_to_events_manual_ack_obsolete.cs | 78 ++
...catching_up_to_normal_events_manual_ack.cs | 67 +-
...up_to_normal_events_manual_ack_obsolete.cs | 65 ++
.../SubscriptionToAll/happy_case_filtered.cs | 65 +-
.../happy_case_filtered_obsolete.cs | 72 ++
...happy_case_filtered_with_start_from_set.cs | 75 +-
...e_filtered_with_start_from_set_obsolete.cs | 86 +++
...subscribing_to_normal_events_manual_ack.cs | 69 +-
...ng_to_normal_events_manual_ack_obsolete.cs | 65 ++
.../update_existing_filtered_obsolete.cs | 30 +
.../update_existing_with_check_point.cs | 178 ++---
...date_existing_with_check_point_filtered.cs | 175 ++---
...ting_with_check_point_filtered_obsolete.cs | 120 +++
.../update_existing_with_subscribers.cs | 66 +-
...date_existing_with_subscribers_obsolete.cs | 60 ++
.../when_writing_and_filtering_out_events.cs | 152 ++--
...iting_and_filtering_out_events_obsolete.cs | 108 +++
...ubscribing_to_normal_events_manual_nack.cs | 72 +-
...g_to_normal_events_manual_nack_obsolete.cs | 68 ++
...connect_to_existing_with_max_one_client.cs | 41 +-
...o_existing_with_max_one_client_obsolete.cs | 47 ++
.../connect_to_existing_with_permissions.cs | 28 +-
...t_to_existing_with_permissions_obsolete.cs | 39 +
...h_start_from_beginning_and_events_in_it.cs | 61 +-
...rom_beginning_and_events_in_it_obsolete.cs | 65 ++
...with_start_from_beginning_and_no_stream.cs | 55 +-
...t_from_beginning_and_no_stream_obsolete.cs | 65 ++
...ith_start_from_not_set_and_events_in_it.cs | 56 +-
..._from_not_set_and_events_in_it_obsolete.cs | 61 ++
...set_and_events_in_it_then_event_written.cs | 52 +-
...vents_in_it_then_event_written_obsolete.cs | 67 ++
...om_set_to_end_position_and_events_in_it.cs | 54 +-
..._end_position_and_events_in_it_obsolete.cs | 60 ++
...ion_and_events_in_it_then_event_written.cs | 64 +-
...vents_in_it_then_event_written_obsolete.cs | 72 ++
...sting_with_start_from_two_and_no_stream.cs | 51 +-
...h_start_from_two_and_no_stream_obsolete.cs | 65 ++
..._with_start_from_x_set_and_events_in_it.cs | 53 +-
...rt_from_x_set_and_events_in_it_obsolete.cs | 65 ++
...set_and_events_in_it_then_event_written.cs | 61 +-
...vents_in_it_then_event_written_obsolete.cs | 68 ++
...n_x_and_events_in_it_then_event_written.cs | 63 +-
...vents_in_it_then_event_written_obsolete.cs | 72 ++
...connect_to_existing_without_permissions.cs | 19 +-
...o_existing_without_permissions_obsolete.cs | 35 +
...onnect_to_non_existing_with_permissions.cs | 29 +-
..._non_existing_with_permissions_obsolete.cs | 34 +
.../connect_with_retries.cs | 78 +-
.../connect_with_retries_obsolete.cs | 70 ++
...connecting_to_a_persistent_subscription.cs | 72 +-
...g_to_a_persistent_subscription_obsolete.cs | 72 ++
.../deleting_existing_with_subscriber.cs | 64 +-
...eting_existing_with_subscriber_obsolete.cs | 67 ++
.../SubscriptionToStream/get_info.cs | 92 +--
.../SubscriptionToStream/get_info_obsolete.cs | 199 +++++
...atching_up_to_link_to_events_manual_ack.cs | 84 +--
...p_to_link_to_events_manual_ack_obsolete.cs | 80 ++
...catching_up_to_normal_events_manual_ack.cs | 71 +-
...up_to_normal_events_manual_ack_obsolete.cs | 67 ++
...subscribing_to_normal_events_manual_ack.cs | 66 +-
...ng_to_normal_events_manual_ack_obsolete.cs | 67 ++
.../update_existing_with_check_point.cs | 167 ++---
...date_existing_with_check_point_obsolete.cs | 124 +++
.../update_existing_with_subscribers.cs | 70 +-
...date_existing_with_subscribers_obsolete.cs | 61 ++
...ubscribing_to_normal_events_manual_nack.cs | 73 +-
...g_to_normal_events_manual_nack_obsolete.cs | 70 ++
.../Bugs/Issue_104.cs | 3 +-
.../Bugs/Issue_2544.cs | 3 +-
.../EventStore.Client.Streams.Tests.csproj | 6 +-
.../Security/SecurityFixture.cs | 16 +-
.../all_stream_with_no_acl_security.cs | 6 +-
.../overriden_system_stream_security.cs | 6 +-
...verriden_system_stream_security_for_all.cs | 5 +-
.../overriden_user_stream_security.cs | 6 +-
.../Security/subscribe_to_all_security.cs | 10 +-
.../Security/subscribe_to_stream_security.cs | 27 +-
.../subscribe_to_stream_security_obsolete.cs | 75 ++
.../Security/system_stream_security.cs | 12 +-
.../Subscriptions/reconnection.cs | 4 +-
.../Subscriptions/subscribe_to_all.cs | 704 +++++++-----------
.../subscribe_to_all_obsolete.cs | 592 +++++++++++++++
.../Subscriptions/subscribe_to_stream.cs | 290 +++-----
.../subscribe_to_stream_obsolete.cs | 303 ++++++++
.../Extensions/TaskExtensions.cs | 2 +-
138 files changed, 7364 insertions(+), 3372 deletions(-)
create mode 100644 src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs
delete mode 100644 src/EventStore.Client.Streams/SubscriptionConfirmation.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers_obsolete.cs
create mode 100644 test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs
create mode 100644 test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security_obsolete.cs
create mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all_obsolete.cs
create mode 100644 test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream_obsolete.cs
diff --git a/Directory.Build.props b/Directory.Build.props
index 5c261f63f..1d320aee3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -13,8 +13,8 @@
pdbonly
true
- 2.59.0
- 2.59.0
+ 2.60.0
+ 2.60.0
diff --git a/samples/persistent-subscriptions/Program.cs b/samples/persistent-subscriptions/Program.cs
index 6ea61fa67..e1e9387c6 100644
--- a/samples/persistent-subscriptions/Program.cs
+++ b/samples/persistent-subscriptions/Program.cs
@@ -7,10 +7,21 @@
await CreatePersistentSubscription(client);
await UpdatePersistentSubscription(client);
-await ConnectToPersistentSubscriptionToStream(client);
+
+try {
+ await ConnectToPersistentSubscriptionToStream(client, GetCT());
+} catch (OperationCanceledException) { }
+
await CreatePersistentSubscriptionToAll(client);
-await ConnectToPersistentSubscriptionToAll(client);
-await ConnectToPersistentSubscriptionWithManualAcks(client);
+
+try {
+ await ConnectToPersistentSubscriptionToAll(client, GetCT());
+} catch (OperationCanceledException) { }
+
+try {
+ await ConnectToPersistentSubscriptionWithManualAcks(client, GetCT());
+} catch (OperationCanceledException) { }
+
await GetPersistentSubscriptionToStreamInfo(client);
await GetPersistentSubscriptionToAllInfo(client);
await ReplayParkedToStream(client);
@@ -39,25 +50,30 @@ await client.CreateToStreamAsync(
);
Console.WriteLine("Subscription to stream created");
+
#endregion create-persistent-subscription-to-stream
}
-static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client) {
+static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client,
+ CancellationToken ct) {
#region subscribe-to-persistent-subscription-to-stream
- var subscription = await client.SubscribeToStreamAsync(
+ await using var subscription = client.SubscribeToStream(
"test-stream",
- "subscription-group",
- async (subscription, evnt, retryCount, cancellationToken) => {
- await HandleEvent(evnt);
- await subscription.Ack(evnt);
- },
- (subscription, dropReason, exception) => {
- Console.WriteLine($"Subscription to stream was dropped due to {dropReason}. {exception?.Message}");
+ "subscription-group",
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId):
+ Console.WriteLine($"Subscription {subscriptionId} to stream started");
+ break;
+ case PersistentSubscriptionMessage.Event(var resolvedEvent, _):
+ await HandleEvent(resolvedEvent);
+ await subscription.Ack(resolvedEvent);
+ break;
}
- );
+ }
- Console.WriteLine("Subscription to stream started");
#endregion subscribe-to-persistent-subscription-to-stream
}
@@ -65,7 +81,7 @@ static async Task CreatePersistentSubscriptionToAll(EventStorePersistentSubscrip
#region create-persistent-subscription-to-all
var userCredentials = new UserCredentials("admin", "changeit");
- var filter = StreamFilter.Prefix("test");
+ var filter = StreamFilter.Prefix("test");
var settings = new PersistentSubscriptionSettings();
await client.CreateToAllAsync(
@@ -76,45 +92,56 @@ await client.CreateToAllAsync(
);
Console.WriteLine("Subscription to all created");
+
#endregion create-persistent-subscription-to-all
}
-static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) {
+static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client,
+ CancellationToken ct) {
#region subscribe-to-persistent-subscription-to-all
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
"subscription-group",
- async (subscription, evnt, retryCount, cancellationToken) => { await HandleEvent(evnt); },
- (subscription, dropReason, exception) => {
- Console.WriteLine($"Subscription to all was dropped due to {dropReason}. {exception?.Message}");
+ cancellationToken: ct);
+
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId):
+ Console.WriteLine($"Subscription {subscriptionId} to stream started");
+ break;
+ case PersistentSubscriptionMessage.Event(var resolvedEvent, _):
+ await HandleEvent(resolvedEvent);
+ break;
}
- );
+ }
- Console.WriteLine("Subscription to all started");
#endregion subscribe-to-persistent-subscription-to-all
}
-static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client) {
+static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client,
+ CancellationToken ct) {
#region subscribe-to-persistent-subscription-with-manual-acks
- var subscription = await client.SubscribeToStreamAsync(
+ await using var subscription = client.SubscribeToStream(
"test-stream",
"subscription-group",
- async (subscription, evnt, retryCount, cancellationToken) => {
- try {
- await HandleEvent(evnt);
- await subscription.Ack(evnt);
- }
- catch (UnrecoverableException ex) {
- await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, evnt);
- }
- },
- (subscription, dropReason, exception) => {
- Console.WriteLine($"Subscription to stream with manual acks was dropped due to {dropReason}. {exception?.Message}");
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId):
+ Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started");
+ break;
+ case PersistentSubscriptionMessage.Event(var resolvedEvent, _):
+ try {
+ await HandleEvent(resolvedEvent);
+ await subscription.Ack(resolvedEvent);
+ } catch (UnrecoverableException ex) {
+ await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent);
+ }
+ break;
}
- );
+ }
- Console.WriteLine("Subscription to stream with manual acks started");
#endregion subscribe-to-persistent-subscription-with-manual-acks
}
@@ -132,6 +159,7 @@ await client.UpdateToStreamAsync(
);
Console.WriteLine("Subscription updated");
+
#endregion update-persistent-subscription
}
@@ -147,14 +175,12 @@ await client.DeleteToStreamAsync(
);
Console.WriteLine("Subscription to stream deleted");
- }
- catch (PersistentSubscriptionNotFoundException) {
+ } catch (PersistentSubscriptionNotFoundException) {
// ignore
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}");
}
-
+
#endregion delete-persistent-subscription
}
@@ -169,11 +195,9 @@ await client.DeleteToAllAsync(
);
Console.WriteLine("Subscription to all deleted");
- }
- catch (PersistentSubscriptionNotFoundException) {
+ } catch (PersistentSubscriptionNotFoundException) {
// ignore
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
Console.WriteLine($"Subscription to all delete error: {ex.GetType()} {ex.Message}");
}
@@ -221,6 +245,7 @@ await client.ReplayParkedMessagesToStreamAsync(
);
Console.WriteLine("Replay of parked messages to stream requested");
+
#endregion persistent-subscription-replay-parked-to-stream
}
@@ -235,6 +260,7 @@ await client.ReplayParkedMessagesToAllAsync(
);
Console.WriteLine("Replay of parked messages to all requested");
+
#endregion replay-parked-of-persistent-subscription-to-all
}
@@ -249,7 +275,7 @@ static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscr
var entries = subscriptions
.Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}");
-
+
Console.WriteLine($"Subscriptions to stream: [ {string.Join(", ", entries)} ]");
#endregion list-persistent-subscriptions-to-stream
@@ -259,13 +285,13 @@ static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscript
#region list-persistent-subscriptions-to-all
var userCredentials = new UserCredentials("admin", "changeit");
- var subscriptions = await client.ListToAllAsync(userCredentials: userCredentials);
+ var subscriptions = await client.ListToAllAsync(userCredentials: userCredentials);
var entries = subscriptions
.Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}");
-
+
Console.WriteLine($"Subscriptions to all: [ {string.Join(", ", entries)} ]");
-
+
#endregion list-persistent-subscriptions-to-all
}
@@ -273,13 +299,13 @@ static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptio
#region list-persistent-subscriptions
var userCredentials = new UserCredentials("admin", "changeit");
- var subscriptions = await client.ListAllAsync(userCredentials: userCredentials);
+ var subscriptions = await client.ListAllAsync(userCredentials: userCredentials);
var entries = subscriptions
.Select(s => $"GroupName: {s.GroupName} EventSource: {s.EventSource} Status: {s.Status}");
-
+
Console.WriteLine($"Subscriptions: [{string.Join(", ", entries)} ]");
-
+
#endregion list-persistent-subscriptions
}
@@ -290,9 +316,14 @@ static async Task RestartPersistentSubscriptionSubsystem(EventStorePersistentSub
await client.RestartSubsystemAsync(userCredentials: userCredentials);
Console.WriteLine("Persistent subscription subsystem restarted");
+
#endregion restart-persistent-subscription-subsystem
}
static Task HandleEvent(ResolvedEvent evnt) => Task.CompletedTask;
-class UnrecoverableException : Exception { }
\ No newline at end of file
+// ensures that samples exit in a timely manner on CI
+static CancellationToken GetCT() => new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token;
+
+class UnrecoverableException : Exception {
+}
diff --git a/samples/server-side-filtering/Program.cs b/samples/server-side-filtering/Program.cs
index 243e40b60..b9e5de8c8 100644
--- a/samples/server-side-filtering/Program.cs
+++ b/samples/server-side-filtering/Program.cs
@@ -8,22 +8,23 @@
await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"));
-await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- semaphore.Release();
- return Task.CompletedTask;
- },
- filterOptions: new SubscriptionFilterOptions(
- EventTypeFilter.Prefix("some-"),
- 1,
- (s, p, c) => {
- Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
- return Task.CompletedTask;
+_ = Task.Run(async () => {
+ await using var subscription = client.SubscribeToAll(
+ FromAll.Start,
+ filterOptions: new SubscriptionFilterOptions(EventTypeFilter.Prefix("some-")));
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ semaphore.Release();
+ break;
+ case StreamMessage.AllStreamCheckpointReached(var p):
+ Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
+ break;
}
- )
-);
+ }
+});
+
await Task.Delay(2000);
@@ -48,14 +49,16 @@ await client.AppendToStreamAsync(
static async Task ExcludeSystemEvents(EventStoreClient client) {
#region exclude-system
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents())
- );
+ filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()));
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ }
+ }
#endregion exclude-system
}
@@ -63,114 +66,107 @@ await client.SubscribeToAllAsync(
static async Task EventTypePrefix(EventStoreClient client) {
#region event-type-prefix
- var filter = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-"));
+ var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-"));
#endregion event-type-prefix
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ }
+ }
}
static async Task EventTypeRegex(EventStoreClient client) {
#region event-type-regex
- var filter = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company"));
+ var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company"));
#endregion event-type-regex
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ }
+ }
}
static async Task StreamPrefix(EventStoreClient client) {
#region stream-prefix
- var filter = new SubscriptionFilterOptions(StreamFilter.Prefix("user-"));
+ var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-"));
#endregion stream-prefix
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ }
+ }
}
static async Task StreamRegex(EventStoreClient client) {
#region stream-regex
- var filter = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings"));
+ var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings"));
#endregion stream-regex
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ }
+ }
}
static async Task CheckpointCallback(EventStoreClient client) {
#region checkpoint
- var filter = new SubscriptionFilterOptions(
- EventTypeFilter.ExcludeSystemEvents(),
- checkpointReached: (s, p, c) => {
- Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
- return Task.CompletedTask;
+ var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents());
+
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ case StreamMessage.AllStreamCheckpointReached(var p):
+ Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
+ break;
}
- );
-
+ }
+
#endregion checkpoint
-
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
}
static async Task CheckpointCallbackWithInterval(EventStoreClient client) {
#region checkpoint-with-interval
- var filter = new SubscriptionFilterOptions(
- EventTypeFilter.ExcludeSystemEvents(),
- 1000,
- (s, p, c) => {
- Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
- return Task.CompletedTask;
- }
- );
+ var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000);
#endregion checkpoint-with-interval
- await client.SubscribeToAllAsync(
- FromAll.Start,
- (s, e, c) => {
- Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
- return Task.CompletedTask;
- },
- filterOptions: filter
- );
-}
\ No newline at end of file
+ await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var e):
+ Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.PreparePosition}");
+ break;
+ case StreamMessage.AllStreamCheckpointReached(var p):
+ Console.WriteLine($"checkpoint taken at {p.PreparePosition}");
+ break;
+ }
+ }
+}
diff --git a/samples/subscribing-to-streams/Program.cs b/samples/subscribing-to-streams/Program.cs
index b0a59ea22..90fa29737 100644
--- a/samples/subscribing-to-streams/Program.cs
+++ b/samples/subscribing-to-streams/Program.cs
@@ -5,91 +5,159 @@
await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"));
-await SubscribeToStream(client);
-await SubscribeToAll(client);
-await OverridingUserCredentials(client);
+await Task.WhenAll(YieldSamples().Select(async sample => {
+ try {
+ await sample;
+ } catch (OperationCanceledException) { }
+}));
-return;
-
-static async Task SubscribeToStream(EventStoreClient client) {
- #region subscribe-to-stream
- await client.SubscribeToStreamAsync(
- "some-stream",
- FromStream.Start,
- async (subscription, evnt, cancellationToken) => {
- Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
- await HandleEvent(evnt);
- }
- );
+return;
- #endregion subscribe-to-stream
+IEnumerable YieldSamples() {
+ yield return SubscribeToStream(client, GetCT());
+ yield return SubscribeToStreamFromPosition(client, GetCT());
+ yield return SubscribeToStreamLive(client, GetCT());
+ yield return SubscribeToStreamResolvingLinkTos(client, GetCT());
+ yield return SubscribeToStreamSubscriptionDropped(client, GetCT());
+ yield return SubscribeToAll(client, GetCT());
+ yield return SubscribeToAllFromPosition(client, GetCT());
+ yield return SubscribeToAllLive(client, GetCT());
+ yield return SubscribeToAllSubscriptionDropped(client, GetCT());
+ yield return OverridingUserCredentials(client, GetCT());
+}
+static async Task SubscribeToStreamFromPosition(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-stream-from-position
- await client.SubscribeToStreamAsync(
+ await using var subscription = client.SubscribeToStream(
"some-stream",
FromStream.After(StreamPosition.FromInt64(20)),
- EventAppeared
- );
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
#endregion subscribe-to-stream-from-position
+}
+static async Task SubscribeToStreamLive(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-stream-live
- await client.SubscribeToStreamAsync(
+ await using var subscription = client.SubscribeToStream(
"some-stream",
FromStream.End,
- EventAppeared
- );
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
#endregion subscribe-to-stream-live
+}
+static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-stream-resolving-linktos
- await client.SubscribeToStreamAsync(
+ await using var subscription = client.SubscribeToStream(
"$et-myEventType",
FromStream.Start,
- EventAppeared,
- true
- );
+ true,
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
#endregion subscribe-to-stream-resolving-linktos
+}
+static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-stream-subscription-dropped
- var checkpoint = await ReadStreamCheckpointAsync();
- await client.SubscribeToStreamAsync(
- "some-stream",
- checkpoint is null ? FromStream.Start : FromStream.After(checkpoint.Value),
- async (subscription, evnt, cancellationToken) => {
- await HandleEvent(evnt);
- checkpoint = evnt.OriginalEventNumber;
- },
- subscriptionDropped: (subscription, reason, exception) => {
- Console.WriteLine($"Subscription was dropped due to {reason}. {exception}");
- if (reason != SubscriptionDroppedReason.Disposed)
- // Resubscribe if the client didn't stop the subscription
- ResubscribeToStream(checkpoint);
+ var checkpoint = await ReadStreamCheckpointAsync() switch {
+ null => FromStream.Start,
+ var position => FromStream.After(position.Value)
+ };
+
+ Subscribe:
+ try {
+ await using var subscription = client.SubscribeToStream(
+ "some-stream",
+ checkpoint,
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ checkpoint = FromStream.After(evnt.OriginalEventNumber);
+ break;
+ }
}
- );
+ } catch (OperationCanceledException) {
+ Console.WriteLine($"Subscription was canceled.");
+ } catch (ObjectDisposedException) {
+ Console.WriteLine($"Subscription was canceled by the user.");
+ } catch (Exception ex) {
+ Console.WriteLine($"Subscription was dropped: {ex}");
+ goto Subscribe;
+ }
#endregion subscribe-to-stream-subscription-dropped
}
-static async Task SubscribeToAll(EventStoreClient client) {
+static async Task SubscribeToStream(EventStoreClient client, CancellationToken ct) {
+ #region subscribe-to-stream
+
+ await using var subscription = client.SubscribeToStream(
+ "some-stream",
+ FromStream.Start,
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages.WithCancellation(ct)) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
+
+ #endregion subscribe-to-stream
+}
+
+static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-all
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
FromAll.Start,
- async (subscription, evnt, cancellationToken) => {
- Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
- await HandleEvent(evnt);
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
}
- );
+ }
#endregion subscribe-to-all
+}
+static async Task SubscribeToAllFromPosition(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-all-from-position
var result = await client.AppendToStreamAsync(
@@ -97,54 +165,98 @@ await client.SubscribeToAllAsync(
StreamState.NoStream,
new[] {
new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty)
- }
- );
+ });
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
FromAll.After(result.LogPosition),
- EventAppeared
- );
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
#endregion subscribe-to-all-from-position
+}
+static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-all-live
- await client.SubscribeToAllAsync(
+ var subscription = client.SubscribeToAll(
FromAll.End,
- EventAppeared
- );
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ break;
+ }
+ }
#endregion subscribe-to-all-live
+}
+static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, CancellationToken ct) {
#region subscribe-to-all-subscription-dropped
- var checkpoint = await ReadCheckpointAsync();
- await client.SubscribeToAllAsync(
- checkpoint is null ? FromAll.Start : FromAll.After(checkpoint.Value),
- async (subscription, evnt, cancellationToken) => {
- await HandleEvent(evnt);
- checkpoint = evnt.OriginalPosition!.Value;
- },
- subscriptionDropped: (subscription, reason, exception) => {
- Console.WriteLine($"Subscription was dropped due to {reason}. {exception}");
- if (reason != SubscriptionDroppedReason.Disposed)
- // Resubscribe if the client didn't stop the subscription
- Resubscribe(checkpoint);
+ var checkpoint = await ReadCheckpointAsync() switch {
+ null => FromAll.Start,
+ var position => FromAll.After(position.Value)
+ };
+
+ Subscribe:
+ try {
+ await using var subscription = client.SubscribeToAll(
+ checkpoint,
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+ if (evnt.OriginalPosition is not null) {
+ checkpoint = FromAll.After(evnt.OriginalPosition.Value);
+ }
+
+ break;
+ }
}
- );
+ } catch (OperationCanceledException) {
+ Console.WriteLine($"Subscription was canceled.");
+ } catch (ObjectDisposedException) {
+ Console.WriteLine($"Subscription was canceled by the user.");
+ } catch (Exception ex) {
+ Console.WriteLine($"Subscription was dropped: {ex}");
+ goto Subscribe;
+ }
#endregion subscribe-to-all-subscription-dropped
}
-static async Task SubscribeToFiltered(EventStoreClient client) {
+static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken ct) {
#region stream-prefix-filtered-subscription
var prefixStreamFilter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-"));
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
FromAll.Start,
- EventAppeared,
- filterOptions: prefixStreamFilter
- );
+ filterOptions: prefixStreamFilter,
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+
+ break;
+ case StreamMessage.AllStreamCheckpointReached(var position):
+ Console.WriteLine($"Checkpoint reached: {position}");
+ break;
+ }
+ }
#endregion stream-prefix-filtered-subscription
@@ -155,31 +267,33 @@ await client.SubscribeToAllAsync(
#endregion stream-regex-filtered-subscription
}
-static async Task OverridingUserCredentials(EventStoreClient client) {
+static async Task OverridingUserCredentials(EventStoreClient client, CancellationToken ct) {
#region overriding-user-credentials
- await client.SubscribeToAllAsync(
+ await using var subscription = client.SubscribeToAll(
FromAll.Start,
- EventAppeared,
- userCredentials: new UserCredentials("admin", "changeit")
- );
+ userCredentials: new UserCredentials("admin", "changeit"),
+ cancellationToken: ct);
+ await foreach (var message in subscription.Messages) {
+ switch (message) {
+ case StreamMessage.Event(var evnt):
+ Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}");
+ await HandleEvent(evnt);
+
+ break;
+ }
+ }
#endregion overriding-user-credentials
}
-static Task EventAppeared(StreamSubscription subscription, ResolvedEvent evnt, CancellationToken cancellationToken) =>
- Task.CompletedTask;
-
-static void SubscriptionDropped(StreamSubscription subscription, SubscriptionDroppedReason reason, Exception ex) { }
-
static Task HandleEvent(ResolvedEvent evnt) => Task.CompletedTask;
-static void ResubscribeToStream(StreamPosition? checkpoint) { }
-
-static void Resubscribe(Position? checkpoint) { }
-
static Task ReadStreamCheckpointAsync() =>
Task.FromResult(new StreamPosition?());
static Task ReadCheckpointAsync() =>
- Task.FromResult(new Position?());
\ No newline at end of file
+ Task.FromResult(new Position?());
+
+// ensures that samples exit in a timely manner on CI
+static CancellationToken GetCT() => new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token;
diff --git a/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs
index 5ad598e4a..98f9de54d 100644
--- a/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs
+++ b/src/EventStore.Client.Common/AsyncStreamReaderExtensions.cs
@@ -1,3 +1,4 @@
+using System.Threading.Channels;
using System.Runtime.CompilerServices;
using Grpc.Core;
@@ -12,4 +13,17 @@ public static async IAsyncEnumerable ReadAllAsync(
while (await reader.MoveNext(cancellationToken).ConfigureAwait(false))
yield return reader.Current;
}
-}
\ No newline at end of file
+
+ public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) {
+#if NET
+ await foreach (var item in reader.ReadAllAsync(cancellationToken))
+ yield return item;
+#else
+ while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) {
+ while (reader.TryRead(out T? item)) {
+ yield return item;
+ }
+ }
+#endif
+ }
+}
diff --git a/src/EventStore.Client.Common/protos/streams.proto b/src/EventStore.Client.Common/protos/streams.proto
index 2037038e5..ac599fb82 100644
--- a/src/EventStore.Client.Common/protos/streams.proto
+++ b/src/EventStore.Client.Common/protos/streams.proto
@@ -4,6 +4,7 @@ option java_package = "com.eventstore.dbclient.proto.streams";
import "shared.proto";
import "status.proto";
+import "google/protobuf/duration.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
@@ -98,8 +99,14 @@ message ReadResp {
uint64 first_stream_position = 5;
uint64 last_stream_position = 6;
AllStreamPosition last_all_stream_position = 7;
+ CaughtUp caught_up = 8;
+ FellBehind fell_behind = 9;
}
+ message CaughtUp {}
+
+ message FellBehind {}
+
message ReadEvent {
RecordedEvent event = 1;
RecordedEvent link = 2;
@@ -214,7 +221,10 @@ message BatchAppendReq {
google.protobuf.Empty any = 4;
google.protobuf.Empty stream_exists = 5;
}
- google.protobuf.Timestamp deadline = 6;
+ oneof deadline_option {
+ google.protobuf.Timestamp deadline_21_10_0 = 6;
+ google.protobuf.Duration deadline = 7;
+ }
}
message ProposedMessage {
@@ -304,4 +314,3 @@ message TombstoneResp {
uint64 prepare_position = 2;
}
}
-
diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs
index 172f33787..d97fa0fc3 100644
--- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs
+++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs
@@ -1,7 +1,7 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.Channels;
using EventStore.Client.PersistentSubscriptions;
+using Grpc.Core;
+using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase;
namespace EventStore.Client {
partial class EventStorePersistentSubscriptionsClient {
@@ -11,7 +11,7 @@ partial class EventStorePersistentSubscriptionsClient {
///
///
///
- [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStreamAsync with manual acks instead.", true)]
+ [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", true)]
public async Task SubscribeAsync(string streamName, string groupName,
Func eventAppeared,
Action? subscriptionDropped = null,
@@ -32,11 +32,29 @@ public async Task SubscribeAsync(string streamName, stri
///
///
///
+ [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream with manual acks instead.", true)]
public async Task SubscribeToStreamAsync(string streamName, string groupName,
- Func eventAppeared,
- Action? subscriptionDropped = null,
- UserCredentials? userCredentials = null, int bufferSize = 10,
- CancellationToken cancellationToken = default) {
+ Func eventAppeared,
+ Action? subscriptionDropped = null,
+ UserCredentials? userCredentials = null, int bufferSize = 10,
+ CancellationToken cancellationToken = default) {
+ return await PersistentSubscription
+ .Confirm(SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken),
+ eventAppeared, subscriptionDropped ?? delegate { }, _log, userCredentials, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Subscribes to a persistent subscription. Messages must be manually acknowledged.
+ ///
+ /// The name of the stream to read events from.
+ /// The name of the persistent subscription group.
+ /// The size of the buffer.
+ /// The optional user credentials to perform operation with.
+ /// The optional .
+ ///
+ public PersistentSubscriptionResult SubscribeToStream(string streamName, string groupName, int bufferSize = 10,
+ UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) {
if (streamName == null) {
throw new ArgumentNullException(nameof(streamName));
}
@@ -45,10 +63,6 @@ public async Task SubscribeToStreamAsync(string streamNa
throw new ArgumentNullException(nameof(groupName));
}
- if (eventAppeared == null) {
- throw new ArgumentNullException(nameof(eventAppeared));
- }
-
if (streamName == string.Empty) {
throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName));
}
@@ -61,17 +75,10 @@ public async Task SubscribeToStreamAsync(string streamNa
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
- var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
-
- if (streamName == SystemStreams.AllStream &&
- !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) {
- throw new NotSupportedException("The server does not support persistent subscriptions to $all.");
- }
-
var readOptions = new ReadReq.Types.Options {
BufferSize = bufferSize,
GroupName = groupName,
- UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()}
+ UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() }
};
if (streamName == SystemStreams.AllStream) {
@@ -80,20 +87,322 @@ public async Task SubscribeToStreamAsync(string streamNa
readOptions.StreamIdentifier = streamName;
}
- return await PersistentSubscription.Confirm(channelInfo.Channel, channelInfo.CallInvoker, Settings, userCredentials, readOptions, _log, eventAppeared,
- subscriptionDropped ?? delegate { }, cancellationToken).ConfigureAwait(false);
+ return new PersistentSubscriptionResult(streamName, groupName, async ct => {
+ var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false);
+
+ if (streamName == SystemStreams.AllStream &&
+ !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) {
+ throw new NotSupportedException("The server does not support persistent subscriptions to $all.");
+ }
+
+ return channelInfo.CallInvoker;
+ }, new() { Options = readOptions }, Settings, userCredentials, cancellationToken);
}
///
/// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged
///
+ [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll with manual acks instead.", true)]
public async Task SubscribeToAllAsync(string groupName,
- Func eventAppeared,
- Action? subscriptionDropped = null,
- UserCredentials? userCredentials = null, int bufferSize = 10,
- CancellationToken cancellationToken = default) =>
+ Func eventAppeared,
+ Action? subscriptionDropped = null,
+ UserCredentials? userCredentials = null, int bufferSize = 10,
+ CancellationToken cancellationToken = default) =>
await SubscribeToStreamAsync(SystemStreams.AllStream, groupName, eventAppeared, subscriptionDropped,
userCredentials, bufferSize, cancellationToken)
.ConfigureAwait(false);
+
+ ///
+ /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged.
+ ///
+ /// The name of the persistent subscription group.
+ /// The size of the buffer.
+ /// The optional user credentials to perform operation with.
+ /// The optional .
+ ///
+ public PersistentSubscriptionResult SubscribeToAll(string groupName, int bufferSize = 10,
+ UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) =>
+ SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken);
+
+ ///
+ public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable {
+ private const int MaxEventIdLength = 2000;
+ private readonly ReadReq _request;
+ private readonly Channel _channel;
+ private readonly CancellationTokenSource _cts;
+ private readonly CallOptions _callOptions;
+
+ private AsyncDuplexStreamingCall? _call;
+ private int _messagesEnumerated;
+
+ ///
+ /// The server-generated unique identifier for the subscription.
+ ///
+ public string? SubscriptionId { get; private set; }
+
+ ///
+ /// The name of the stream to read events from.
+ ///
+ public string StreamName { get; }
+
+ ///
+ /// The name of the persistent subscription group.
+ ///
+ public string GroupName { get; }
+
+ ///
+ /// An . Do not enumerate more than once.
+ ///
+ public IAsyncEnumerable Messages {
+ get {
+ if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) {
+ throw new InvalidOperationException("Messages may only be enumerated once.");
+ }
+
+ return GetMessages();
+
+ async IAsyncEnumerable GetMessages() {
+ try {
+ await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) {
+ if (message is PersistentSubscriptionMessage
+ .SubscriptionConfirmation(var subscriptionId)) {
+ SubscriptionId = subscriptionId;
+ }
+
+ yield return message;
+ }
+ } finally {
+ _cts.Cancel();
+ }
+ }
+ }
+ }
+
+ internal PersistentSubscriptionResult(string streamName, string groupName,
+ Func> selectCallInvoker,
+ ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials,
+ CancellationToken cancellationToken) {
+ StreamName = streamName;
+ GroupName = groupName;
+
+ _request = request;
+
+ _callOptions = EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials,
+ cancellationToken: cancellationToken);
+
+ _channel = Channel.CreateBounded(ReadBoundedChannelOptions);
+
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+
+ _ = PumpMessages();
+
+ return;
+
+ async Task PumpMessages() {
+ try {
+ var callInvoker = await selectCallInvoker(_cts.Token).ConfigureAwait(false);
+ var client = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(
+ callInvoker);
+ _call = client.Read(_callOptions);
+
+ await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false);
+
+ await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token)
+ .ConfigureAwait(false)) {
+ await _channel.Writer.WriteAsync(response.ContentCase switch {
+ SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation(
+ response.SubscriptionConfirmation.SubscriptionId),
+ Event => new PersistentSubscriptionMessage.Event(ConvertToResolvedEvent(response),
+ response.Event.CountCase switch {
+ ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount,
+ _ => null
+ }),
+ _ => PersistentSubscriptionMessage.Unknown.Instance
+ }, _cts.Token).ConfigureAwait(false);
+ }
+
+ _channel.Writer.TryComplete();
+ } catch (Exception ex) {
+#if NET48
+ switch (ex) {
+ // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests.
+ // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown.
+ // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030.
+ // Additionally, there are cases where the server response does not include the 'grpc-status' header.
+ // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response".
+ // The switch statement below handles these specific exceptions and translates them into the appropriate
+ // PersistentSubscriptionDroppedByServerException exception.
+ case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"):
+ case RpcException { StatusCode: StatusCode.Cancelled } rex2
+ when rex2.Status.Detail.Contains("No grpc-status found on response"):
+ ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex);
+ break;
+ }
+#endif
+ if (ex is PersistentSubscriptionNotFoundException) {
+ await _channel.Writer.WriteAsync(PersistentSubscriptionMessage.NotFound.Instance,
+ cancellationToken).ConfigureAwait(false);
+ _channel.Writer.TryComplete();
+ return;
+ }
+ _channel.Writer.TryComplete(ex);
+ }
+ }
+ }
+
+ ///
+ /// Acknowledge that a message has completed processing (this will tell the server it has been processed).
+ ///
+ /// There is no need to ack a message if you have Auto Ack enabled.
+ /// The of the s to acknowledge. There should not be more than 2000 to ack at a time.
+ public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds);
+
+ ///
+ /// Acknowledge that a message has completed processing (this will tell the server it has been processed).
+ ///
+ /// There is no need to ack a message if you have Auto Ack enabled.
+ /// The of the s to acknowledge. There should not be more than 2000 to ack at a time.
+ public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray());
+
+ ///
+ /// Acknowledge that a message has completed processing (this will tell the server it has been processed).
+ ///
+ /// There is no need to ack a message if you have Auto Ack enabled.
+ /// The s to acknowledge. There should not be more than 2000 to ack at a time.
+ public Task Ack(params ResolvedEvent[] resolvedEvents) =>
+ Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId));
+
+ ///
+ /// Acknowledge that a message has completed processing (this will tell the server it has been processed).
+ ///
+ /// There is no need to ack a message if you have Auto Ack enabled.
+ /// The s to acknowledge. There should not be more than 2000 to ack at a time.
+ public Task Ack(IEnumerable resolvedEvents) =>
+ Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId));
+
+
+ ///
+ /// Acknowledge that a message has failed processing (this will tell the server it has not been processed).
+ ///
+ /// The to take.
+ /// A reason given.
+ /// The of the s to nak. There should not be more than 2000 to nak at a time.
+ /// The number of eventIds exceeded the limit of 2000.
+ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) =>
+ NackInternal(eventIds, action, reason);
+
+ ///
+ /// Acknowledge that a message has failed processing (this will tell the server it has not been processed).
+ ///
+ /// The to take.
+ /// A reason given.
+ /// The s to nak. There should not be more than 2000 to nak at a time.
+ /// The number of resolvedEvents exceeded the limit of 2000.
+ public Task Nack(PersistentSubscriptionNakEventAction action, string reason,
+ params ResolvedEvent[] resolvedEvents) => Nack(action, reason,
+ Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId));
+
+ private static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new(
+ ConvertToEventRecord(response.Event.Event)!,
+ ConvertToEventRecord(response.Event.Link),
+ response.Event.PositionCase switch {
+ ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition,
+ _ => null
+ });
+
+ private Task AckInternal(params Uuid[] eventIds) {
+ if (eventIds.Length > MaxEventIdLength) {
+ throw new ArgumentException(
+ $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", nameof(eventIds));
+ }
+
+ return _call is null
+ ? throw new InvalidOperationException()
+ : _call.RequestStream.WriteAsync(new ReadReq {
+ Ack = new ReadReq.Types.Ack {
+ Ids = {
+ Array.ConvertAll(eventIds, id => id.ToDto())
+ }
+ }
+ });
+ }
+
+ private Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) {
+ if (eventIds.Length > MaxEventIdLength) {
+ throw new ArgumentException(
+ $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", nameof(eventIds));
+ }
+
+ return _call is null
+ ? throw new InvalidOperationException()
+ : _call.RequestStream.WriteAsync(new ReadReq {
+ Nack = new ReadReq.Types.Nack {
+ Ids = {
+ Array.ConvertAll(eventIds, id => id.ToDto())
+ },
+ Action = action switch {
+ PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park,
+ PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry,
+ PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip,
+ PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop,
+ _ => ReadReq.Types.Nack.Types.Action.Unknown
+ },
+ Reason = reason
+ }
+ });
+ }
+
+ private static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) =>
+ e is null
+ ? null
+ : new EventRecord(
+ e.StreamIdentifier!,
+ Uuid.FromDto(e.Id),
+ new StreamPosition(e.StreamRevision),
+ new Position(e.CommitPosition, e.PreparePosition),
+ e.Metadata,
+ e.Data.ToByteArray(),
+ e.CustomMetadata.ToByteArray());
+
+ ///
+ public async ValueTask DisposeAsync() {
+ await CastAndDispose(_cts).ConfigureAwait(false);
+ await CastAndDispose(_call).ConfigureAwait(false);
+
+ return;
+
+ static async Task CastAndDispose(IDisposable? resource) {
+ switch (resource) {
+ case null:
+ return;
+ case IAsyncDisposable resourceAsyncDisposable:
+ await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false);
+ break;
+ default:
+ resource.Dispose();
+ break;
+ }
+ }
+ }
+
+
+ ///
+ public void Dispose() {
+ _cts.Dispose();
+ _call?.Dispose();
+ }
+
+ ///
+ public async IAsyncEnumerator GetAsyncEnumerator(
+ CancellationToken cancellationToken = default) {
+ await foreach (var message in Messages.WithCancellation(cancellationToken)) {
+ if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) {
+ continue;
+ }
+
+ yield return resolvedEvent;
+ }
+ }
+ }
}
}
diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs
index 1e14a5686..411de3e70 100644
--- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs
+++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs
@@ -1,7 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Text.Encodings.Web;
+using System.Threading.Channels;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -11,6 +9,12 @@ namespace EventStore.Client {
/// The client used to manage persistent subscriptions in the EventStoreDB.
///
public sealed partial class EventStorePersistentSubscriptionsClient : EventStoreClientBase {
+ private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) {
+ SingleReader = true,
+ SingleWriter = true,
+ AllowSynchronousContinuations = true
+ };
+
private readonly ILogger _log;
///
diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs
index 56b8d305d..1e79b9363 100644
--- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs
+++ b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs
@@ -1,8 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using EventStore.Client.PersistentSubscriptions;
using Grpc.Core;
using Microsoft.Extensions.Logging;
@@ -11,13 +6,14 @@ namespace EventStore.Client {
///
/// Represents a persistent subscription connection.
///
+ [Obsolete]
public class PersistentSubscription : IDisposable {
+ private readonly EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult;
+ private readonly IAsyncEnumerator _enumerator;
private readonly Func _eventAppeared;
- private readonly Action _subscriptionDropped;
- private readonly IDisposable _disposable;
- private readonly CancellationToken _cancellationToken;
- private readonly AsyncDuplexStreamingCall _call;
- private readonly ILogger _log;
+ private readonly Action _subscriptionDropped;
+ private readonly ILogger _log;
+ private readonly CancellationTokenSource _cts;
private int _subscriptionDroppedInvoked;
@@ -27,47 +23,43 @@ public class PersistentSubscription : IDisposable {
public string SubscriptionId { get; }
internal static async Task Confirm(
- ChannelBase channel, CallInvoker callInvoker, EventStoreClientSettings settings,
- UserCredentials? userCredentials, ReadReq.Types.Options options, ILogger log,
+ EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult,
Func eventAppeared,
Action subscriptionDropped,
- CancellationToken cancellationToken = default) {
-
- var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-
- var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker)
- .Read(EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials,
- cancellationToken: cts.Token));
-
- await call.RequestStream.WriteAsync(new ReadReq {
- Options = options
- }).ConfigureAwait(false);
-
- if (await call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false) &&
- call.ResponseStream.Current.ContentCase == ReadResp.ContentOneofCase.SubscriptionConfirmation)
- return new PersistentSubscription(call, log, eventAppeared, subscriptionDropped, cts.Token, cts);
-
- call.Dispose();
- cts.Dispose();
- throw new InvalidOperationException("Subscription could not be confirmed.");
+ ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) {
+ var enumerator = persistentSubscriptionResult
+ .Messages
+ .GetAsyncEnumerator(cancellationToken);
+
+ var result = await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false);
+
+ return (result, enumerator.Current) switch {
+ (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) =>
+ new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared,
+ subscriptionDropped, log, cancellationToken),
+ (true, PersistentSubscriptionMessage.NotFound) =>
+ throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName,
+ persistentSubscriptionResult.GroupName),
+ _ => throw new InvalidOperationException("Subscription could not be confirmed.")
+ };
}
// PersistentSubscription takes responsibility for disposing the call and the disposable
private PersistentSubscription(
- AsyncDuplexStreamingCall call,
- ILogger log,
+ EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult,
+ IAsyncEnumerator enumerator, string subscriptionId,
Func eventAppeared,
- Action subscriptionDropped,
- CancellationToken cancellationToken,
- IDisposable disposable) {
- _call = call;
- _eventAppeared = eventAppeared;
+ Action subscriptionDropped, ILogger log,
+ CancellationToken cancellationToken) {
+ _persistentSubscriptionResult = persistentSubscriptionResult;
+ _enumerator = enumerator;
+ SubscriptionId = subscriptionId;
+ _eventAppeared = eventAppeared;
_subscriptionDropped = subscriptionDropped;
- _cancellationToken = cancellationToken;
- _disposable = disposable;
- _log = log;
- SubscriptionId = call.ResponseStream.Current.SubscriptionConfirmation.SubscriptionId;
- Task.Run(Subscribe);
+ _log = log;
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+
+ Task.Run(Subscribe, _cts.Token);
}
///
@@ -75,13 +67,7 @@ private PersistentSubscription(
///
/// There is no need to ack a message if you have Auto Ack enabled.
/// The of the s to acknowledge. There should not be more than 2000 to ack at a time.
- public Task Ack(params Uuid[] eventIds) {
- if (eventIds.Length > 2000) {
- throw new ArgumentException();
- }
-
- return AckInternal(eventIds);
- }
+ public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds);
///
/// Acknowledge that a message has completed processing (this will tell the server it has been processed).
@@ -114,13 +100,7 @@ public Task Ack(IEnumerable resolvedEvents) =>
/// A reason given.
/// The of the s to nak. There should not be more than 2000 to nak at a time.
/// The number of eventIds exceeded the limit of 2000.
- public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) {
- if (eventIds.Length > 2000) {
- throw new ArgumentException();
- }
-
- return NackInternal(eventIds, action, reason);
- }
+ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason);
///
/// Acknowledge that a message has failed processing (this will tell the server it has not been processed).
@@ -130,7 +110,7 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par
/// The s to nak. There should not be more than 2000 to nak at a time.
/// The number of resolvedEvents exceeded the limit of 2000.
public Task Nack(PersistentSubscriptionNakEventAction action, string reason,
- params ResolvedEvent[] resolvedEvents) =>
+ params ResolvedEvent[] resolvedEvents) =>
Nack(action, reason,
Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId));
@@ -141,12 +121,21 @@ private async Task Subscribe() {
_log.LogDebug("Persistent Subscription {subscriptionId} confirmed.", SubscriptionId);
try {
- await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) {
- if (response.ContentCase != ReadResp.ContentOneofCase.Event) {
+ while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) {
+ if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) {
continue;
}
- var resolvedEvent = ConvertToResolvedEvent(response);
+ if (_enumerator.Current is PersistentSubscriptionMessage.NotFound) {
+ if (_subscriptionDroppedInvoked != 0) {
+ return;
+ }
+ SubscriptionDropped(SubscriptionDroppedReason.ServerError,
+ new PersistentSubscriptionNotFoundException(
+ _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName));
+ return;
+ }
+
_log.LogTrace(
"Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}",
SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId,
@@ -156,13 +145,8 @@ private async Task Subscribe() {
await _eventAppeared(
this,
resolvedEvent,
- response.Event.CountCase switch {
- ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event
- .RetryCount,
- _ => default
- },
- _cancellationToken).ConfigureAwait(false);
-
+ retryCount,
+ _cts.Token).ConfigureAwait(false);
} catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) {
if (_subscriptionDroppedInvoked != 0) {
return;
@@ -186,23 +170,6 @@ await _eventAppeared(
}
} catch (Exception ex) {
if (_subscriptionDroppedInvoked == 0) {
-#if NET48
- switch (ex) {
- // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests.
- // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown.
- // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030.
- // Additionally, there are cases where the server response does not include the 'grpc-status' header.
- // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response".
- // The switch statement below handles these specific exceptions and translates them into the appropriate
- // PersistentSubscriptionDroppedByServerException exception. The downside of this approach is that it does not return the stream name
- // and group name.
- case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"):
- case RpcException { StatusCode: StatusCode.Cancelled } rex2
- when rex2.Status.Detail.Contains("No grpc-status found on response"):
- ex = new PersistentSubscriptionDroppedByServerException("", "", ex);
- break;
- }
-#endif
_log.LogError(ex,
"Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.",
SubscriptionId);
@@ -216,26 +183,6 @@ when rex2.Status.Detail.Contains("No grpc-status found on response"):
SubscriptionDropped(SubscriptionDroppedReason.ServerError);
}
}
-
- ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new(
- ConvertToEventRecord(response.Event.Event)!,
- ConvertToEventRecord(response.Event.Link),
- response.Event.PositionCase switch {
- ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition,
- _ => null
- });
-
- EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) =>
- e == null
- ? null
- : new EventRecord(
- e.StreamIdentifier!,
- Uuid.FromDto(e.Id),
- new StreamPosition(e.StreamRevision),
- new Position(e.CommitPosition, e.PreparePosition),
- e.Metadata,
- e.Data.ToByteArray(),
- e.CustomMetadata.ToByteArray());
}
private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex = null) {
@@ -246,35 +193,14 @@ private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex
try {
_subscriptionDropped.Invoke(this, reason, ex);
} finally {
- _call.Dispose();
- _disposable.Dispose();
+ _persistentSubscriptionResult.Dispose();
+ _cts.Dispose();
}
}
- private Task AckInternal(params Uuid[] ids) =>
- _call.RequestStream.WriteAsync(new ReadReq {
- Ack = new ReadReq.Types.Ack {
- Ids = {
- Array.ConvertAll(ids, id => id.ToDto())
- }
- }
- });
+ private Task AckInternal(params Uuid[] ids) => _persistentSubscriptionResult.Ack(ids);
private Task NackInternal(Uuid[] ids, PersistentSubscriptionNakEventAction action, string reason) =>
- _call!.RequestStream.WriteAsync(new ReadReq {
- Nack = new ReadReq.Types.Nack {
- Ids = {
- Array.ConvertAll(ids, id => id.ToDto())
- },
- Action = action switch {
- PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park,
- PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry,
- PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip,
- PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop,
- _ => ReadReq.Types.Nack.Types.Action.Unknown
- },
- Reason = reason
- }
- });
+ _persistentSubscriptionResult.Nack(action, reason, ids);
}
-}
\ No newline at end of file
+}
diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs
new file mode 100644
index 000000000..eabde9b62
--- /dev/null
+++ b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs
@@ -0,0 +1,33 @@
+namespace EventStore.Client {
+ ///
+ /// The base record of all stream messages.
+ ///
+ public abstract record PersistentSubscriptionMessage {
+ ///
+ /// A that represents a .
+ ///
+ /// The .
+ /// The number of times the has been retried.
+ public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage;
+
+ ///
+ /// A representing a stream that was not found.
+ ///
+ public record NotFound : PersistentSubscriptionMessage {
+ internal static readonly NotFound Instance = new();
+ }
+
+ ///
+ /// A indicating that the subscription is ready to send additional messages.
+ ///
+ /// The unique identifier of the subscription.
+ public record SubscriptionConfirmation(string SubscriptionId) : PersistentSubscriptionMessage;
+
+ ///
+ /// A that could not be identified, usually indicating a lower client compatibility level than the server supports.
+ ///
+ public record Unknown : PersistentSubscriptionMessage {
+ internal static readonly Unknown Instance = new();
+ }
+ }
+}
diff --git a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj
index a9c2969f2..4aa697465 100644
--- a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj
+++ b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj
@@ -3,9 +3,6 @@
The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/
-
-
-
diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs
index 2cf59db32..9d0842855 100644
--- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs
+++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs
@@ -293,7 +293,7 @@ private async Task Send() {
if (call is null)
throw new NotSupportedException("Server does not support batch append");
- await foreach (var appendRequest in ReadAllAsync(_channel.Reader, _cancellationToken)
+ await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken)
.ConfigureAwait(false)) {
await call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false);
}
@@ -364,18 +364,5 @@ public void Dispose() {
_channel.Writer.TryComplete();
}
}
-
- private static async IAsyncEnumerable ReadAllAsync(ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) {
-#if NET
- await foreach (var item in reader.ReadAllAsync(cancellationToken))
- yield return item;
-#else
- while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) {
- while (reader.TryRead(out T? item)) {
- yield return item;
- }
- }
-#endif
- }
}
}
diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs
index 92dad2c27..d79b83bc6 100644
--- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs
+++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs
@@ -135,7 +135,8 @@ async IAsyncEnumerable GetMessages() {
}
try {
- await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) {
+ await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)
+ .ConfigureAwait(false)) {
if (message is StreamMessage.LastAllStreamPosition(var position)) {
LastPosition = position;
}
@@ -155,11 +156,7 @@ internal ReadAllStreamResult(Func> selectCa
var callOptions = EventStoreCallOptions.CreateStreaming(settings, deadline, userCredentials,
cancellationToken);
- _channel = Channel.CreateBounded(new BoundedChannelOptions(1) {
- SingleReader = true,
- SingleWriter = true,
- AllowSynchronousContinuations = true
- });
+ _channel = Channel.CreateBounded(ReadBoundedChannelOptions);
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var linkedCancellationToken = _cts.Token;
@@ -199,7 +196,7 @@ public async IAsyncEnumerator GetAsyncEnumerator(
CancellationToken cancellationToken = default) {
try {
- await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) {
+ await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) {
if (message is not StreamMessage.Event e) {
continue;
}
@@ -298,7 +295,7 @@ async IAsyncEnumerable GetMessages() {
}
try {
- await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) {
+ await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) {
switch (message) {
case StreamMessage.FirstStreamPosition(var streamPosition):
FirstStreamPosition = streamPosition;
@@ -330,11 +327,7 @@ internal ReadStreamResult(Func> selectCallI
var callOptions = EventStoreCallOptions.CreateStreaming(settings, deadline, userCredentials,
cancellationToken);
- _channel = Channel.CreateBounded(new BoundedChannelOptions(1) {
- SingleReader = true,
- SingleWriter = true,
- AllowSynchronousContinuations = true
- });
+ _channel = Channel.CreateBounded(ReadBoundedChannelOptions);
StreamName = request.Options.Stream.StreamIdentifier!;
@@ -396,7 +389,7 @@ public async IAsyncEnumerator GetAsyncEnumerator(
CancellationToken cancellationToken = default) {
try {
- await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) {
+ await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) {
if (message is StreamMessage.NotFound) {
throw new StreamNotFoundException(StreamName);
}
@@ -412,55 +405,7 @@ public async IAsyncEnumerator GetAsyncEnumerator(
}
}
}
-
- private async IAsyncEnumerable<(SubscriptionConfirmation, Position?, ResolvedEvent)> ReadInternal(
- ReadReq request,
- UserCredentials? userCredentials,
- [EnumeratorCancellation] CancellationToken cancellationToken) {
- if (request.Options.CountOptionCase == ReadReq.Types.Options.CountOptionOneofCase.Count &&
- request.Options.Count <= 0) {
- throw new ArgumentOutOfRangeException("count");
- }
-
- if (request.Options.Filter == null) {
- request.Options.NoFilter = new Empty();
- }
-
- request.Options.UuidOption = new ReadReq.Types.Options.Types.UUIDOption {Structured = new Empty()};
-
- var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
-
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-
- using var call = new Streams.Streams.StreamsClient(
- channelInfo.CallInvoker).Read(request,
- EventStoreCallOptions.CreateStreaming(Settings, userCredentials: userCredentials,
- cancellationToken: cts.Token));
-
- await foreach (var e in call.ResponseStream
- .ReadAllAsync(cts.Token)
- .Select(ConvertToItem)
- .WithCancellation(cts.Token)
- .ConfigureAwait(false)) {
- if (e.HasValue) {
- yield return e.Value;
- }
- }
- }
-
- private static (SubscriptionConfirmation, Position?, ResolvedEvent)? ConvertToItem(ReadResp response) =>
- response.ContentCase switch {
- Confirmation => (
- new SubscriptionConfirmation(response.Confirmation.SubscriptionId), null, default),
- Event => (SubscriptionConfirmation.None,
- null,
- ConvertToResolvedEvent(response.Event)),
- Checkpoint => (SubscriptionConfirmation.None,
- new Position(response.Checkpoint.CommitPosition, response.Checkpoint.PreparePosition),
- default),
- _ => null
- };
-
+
private static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) =>
new ResolvedEvent(
ConvertToEventRecord(readEvent.Event)!,
diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs
index 47ea634f1..78013f31c 100644
--- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs
+++ b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs
@@ -1,7 +1,7 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.Channels;
using EventStore.Client.Streams;
+using Grpc.Core;
+using static EventStore.Client.Streams.ReadResp.ContentOneofCase;
namespace EventStore.Client {
public partial class EventStoreClient {
@@ -16,6 +16,7 @@ public partial class EventStoreClient {
/// The optional user credentials to perform operation with.
/// The optional .
///
+ [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll instead.", true)]
public Task SubscribeToAllAsync(
FromAll start,
Func eventAppeared,
@@ -23,16 +24,38 @@ public Task SubscribeToAllAsync(
Action? subscriptionDropped = default,
SubscriptionFilterOptions? filterOptions = null,
UserCredentials? userCredentials = null,
- CancellationToken cancellationToken = default) => StreamSubscription.Confirm(ReadInternal(new ReadReq {
- Options = new ReadReq.Types.Options {
- ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards,
- ResolveLinks = resolveLinkTos,
- All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start),
- Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(),
- Filter = GetFilterOptions(filterOptions)!
- }
- }, userCredentials, cancellationToken), eventAppeared, subscriptionDropped, _log,
- filterOptions?.CheckpointReached, cancellationToken);
+ CancellationToken cancellationToken = default) => StreamSubscription.Confirm(
+ SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken),
+ eventAppeared, subscriptionDropped, _log, filterOptions?.CheckpointReached,
+ cancellationToken: cancellationToken);
+
+ ///
+ /// Subscribes to all events.
+ ///
+ /// A (exclusive of) to start the subscription from.
+ /// Whether to resolve LinkTo events automatically.
+ /// The optional to apply.
+ /// The optional user credentials to perform operation with.
+ /// The optional .
+ ///
+ public StreamSubscriptionResult SubscribeToAll(
+ FromAll start,
+ bool resolveLinkTos = false,
+ SubscriptionFilterOptions? filterOptions = null,
+ UserCredentials? userCredentials = null,
+ CancellationToken cancellationToken = default) => new(async _ => {
+ var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
+ return channelInfo.CallInvoker;
+ }, new ReadReq {
+ Options = new ReadReq.Types.Options {
+ ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards,
+ ResolveLinks = resolveLinkTos,
+ All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start),
+ Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(),
+ Filter = GetFilterOptions(filterOptions)!,
+ UuidOption = new() { Structured = new() }
+ }
+ }, Settings, userCredentials, cancellationToken);
///
/// Subscribes to a stream from a checkpoint.
@@ -45,20 +68,184 @@ public Task SubscribeToAllAsync(
/// The optional user credentials to perform operation with.
/// The optional .
///
+ [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream instead.", true)]
public Task SubscribeToStreamAsync(string streamName,
+ FromStream start,
+ Func eventAppeared,
+ bool resolveLinkTos = false,
+ Action? subscriptionDropped = default,
+ UserCredentials? userCredentials = null,
+ CancellationToken cancellationToken = default) => StreamSubscription.Confirm(
+ SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken),
+ eventAppeared, subscriptionDropped, _log, cancellationToken: cancellationToken);
+
+ ///
+ /// Subscribes to a stream from a checkpoint.
+ ///
+ /// A (exclusive of) to start the subscription from.
+ /// The name of the stream to read events from.
+ /// Whether to resolve LinkTo events automatically.
+ /// The optional user credentials to perform operation with.
+ /// The optional .
+ ///
+ public StreamSubscriptionResult SubscribeToStream(
+ string streamName,
FromStream start,
- Func eventAppeared,
bool resolveLinkTos = false,
- Action? subscriptionDropped = default,
UserCredentials? userCredentials = null,
- CancellationToken cancellationToken = default) => StreamSubscription.Confirm(ReadInternal(new ReadReq {
- Options = new ReadReq.Types.Options {
- ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards,
- ResolveLinks = resolveLinkTos,
- Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start),
- Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions()
+ CancellationToken cancellationToken = default) => new(async _ => {
+ var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false);
+ return channelInfo.CallInvoker;
+ }, new ReadReq {
+ Options = new ReadReq.Types.Options {
+ ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards,
+ ResolveLinks = resolveLinkTos,
+ Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start),
+ Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(),
+ UuidOption = new() { Structured = new() }
+ }
+ }, Settings, userCredentials, cancellationToken);
+
+ ///
+ /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once.
+ ///
+ public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable {
+ private readonly ReadReq _request;
+ private readonly Channel _channel;
+ private readonly CancellationTokenSource _cts;
+ private readonly CallOptions _callOptions;
+ private AsyncServerStreamingCall? _call;
+
+ private int _messagesEnumerated;
+
+ ///
+ /// The server-generated unique identifier for the subscription.
+ ///
+ public string? SubscriptionId { get; private set; }
+
+ ///
+ /// An . Do not enumerate more than once.
+ ///
+ public IAsyncEnumerable Messages {
+ get {
+ if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) {
+ throw new InvalidOperationException("Messages may only be enumerated once.");
+ }
+
+ return GetMessages();
+
+ async IAsyncEnumerable GetMessages() {
+ try {
+ await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) {
+ if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) {
+ SubscriptionId = subscriptionId;
+ }
+
+ yield return message;
+ }
+ } finally {
+ _cts.Cancel();
+ }
+ }
}
- }, userCredentials, cancellationToken), eventAppeared, subscriptionDropped, _log,
- cancellationToken: cancellationToken);
+ }
+
+ internal StreamSubscriptionResult(Func> selectCallInvoker,
+ ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials,
+ CancellationToken cancellationToken) {
+ _request = request;
+ _callOptions = EventStoreCallOptions.CreateStreaming(settings, userCredentials: userCredentials,
+ cancellationToken: cancellationToken);
+
+ _channel = Channel.CreateBounded(ReadBoundedChannelOptions);
+
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+
+ if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) {
+ _request.Options.NoFilter = new();
+ }
+
+ _ = PumpMessages();
+
+ return;
+
+ async Task PumpMessages() {
+ try {
+ var callInvoker = await selectCallInvoker(_cts.Token).ConfigureAwait(false);
+ var client = new Streams.Streams.StreamsClient(callInvoker);
+ _call = client.Read(_request, _callOptions);
+ await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token)
+ .ConfigureAwait(false)) {
+ await _channel.Writer.WriteAsync(response.ContentCase switch {
+ Confirmation => new StreamMessage.SubscriptionConfirmation(
+ response.Confirmation.SubscriptionId),
+ Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)),
+ FirstStreamPosition => new StreamMessage.FirstStreamPosition(
+ new StreamPosition(response.FirstStreamPosition)),
+ LastStreamPosition => new StreamMessage.LastStreamPosition(
+ new StreamPosition(response.LastStreamPosition)),
+ LastAllStreamPosition => new StreamMessage.LastAllStreamPosition(
+ new Position(response.LastAllStreamPosition.CommitPosition,
+ response.LastAllStreamPosition.PreparePosition)),
+ Checkpoint => new StreamMessage.AllStreamCheckpointReached(
+ new Position(response.Checkpoint.CommitPosition,
+ response.Checkpoint.PreparePosition)),
+ CaughtUp => StreamMessage.CaughtUp.Instance,
+ FellBehind => StreamMessage.FellBehind.Instance,
+ _ => StreamMessage.Unknown.Instance
+ }, _cts.Token).ConfigureAwait(false);
+ }
+
+ _channel.Writer.Complete();
+ } catch (Exception ex) {
+ _channel.Writer.TryComplete(ex);
+ }
+ }
+ }
+
+ ///
+ public async ValueTask DisposeAsync() {
+ await CastAndDispose(_cts).ConfigureAwait(false);
+ await CastAndDispose(_call).ConfigureAwait(false);
+
+ return;
+
+ static async ValueTask CastAndDispose(IDisposable? resource) {
+ switch (resource) {
+ case null:
+ return;
+ case IAsyncDisposable resourceAsyncDisposable:
+ await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false);
+ break;
+ default:
+ resource.Dispose();
+ break;
+ }
+ }
+ }
+
+ ///
+ public void Dispose() {
+ _cts.Dispose();
+ _call?.Dispose();
+ }
+
+ ///
+ public async IAsyncEnumerator GetAsyncEnumerator(
+ CancellationToken cancellationToken = default) {
+ try {
+ await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken)
+ .ConfigureAwait(false)) {
+ if (message is not StreamMessage.Event e) {
+ continue;
+ }
+
+ yield return e.ResolvedEvent;
+ }
+ } finally {
+ _cts.Cancel();
+ }
+ }
+ }
}
}
diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/EventStore.Client.Streams/EventStoreClient.cs
index e9d7fd85c..361e6e2d4 100644
--- a/src/EventStore.Client.Streams/EventStoreClient.cs
+++ b/src/EventStore.Client.Streams/EventStoreClient.cs
@@ -1,9 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.Channels;
using EventStore.Client.Streams;
using Grpc.Core;
using Microsoft.Extensions.Logging;
@@ -23,6 +19,12 @@ public sealed partial class EventStoreClient : EventStoreClientBase {
},
};
+ private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) {
+ SingleReader = true,
+ SingleWriter = true,
+ AllowSynchronousContinuations = true
+ };
+
private readonly ILogger _log;
private Lazy _streamAppenderLazy;
private StreamAppender _streamAppender => _streamAppenderLazy.Value;
diff --git a/src/EventStore.Client.Streams/StreamMessage.cs b/src/EventStore.Client.Streams/StreamMessage.cs
index f6b564e71..4f1a87559 100644
--- a/src/EventStore.Client.Streams/StreamMessage.cs
+++ b/src/EventStore.Client.Streams/StreamMessage.cs
@@ -41,6 +41,38 @@ public record LastStreamPosition(StreamPosition StreamPosition) : StreamMessage;
/// The .
public record LastAllStreamPosition(Position Position) : StreamMessage;
+ ///
+ /// A indicating that the subscription is ready to send additional messages.
+ ///
+ /// The unique identifier of the subscription.
+ public record SubscriptionConfirmation(string SubscriptionId) : StreamMessage;
+
+ ///
+ /// A indicating that a checkpoint has been reached.
+ ///
+ /// The .
+ public record AllStreamCheckpointReached(Position Position) : StreamMessage;
+
+ ///
+ /// A indicating that a checkpoint has been reached.
+ ///
+ /// The .
+ public record StreamCheckpointReached(StreamPosition StreamPosition) : StreamMessage;
+
+ ///
+ /// A indicating that the subscription is live.
+ ///
+ public record CaughtUp : StreamMessage {
+ internal static readonly CaughtUp Instance = new();
+ }
+
+ ///
+ /// A indicating that the subscription has switched to catch up mode.
+ ///
+ public record FellBehind : StreamMessage {
+ internal static readonly FellBehind Instance = new();
+ }
+
///
/// A that could not be identified, usually indicating a lower client compatibility level than the server supports.
///
diff --git a/src/EventStore.Client.Streams/StreamSubscription.cs b/src/EventStore.Client.Streams/StreamSubscription.cs
index db21c551a..9019eda64 100644
--- a/src/EventStore.Client.Streams/StreamSubscription.cs
+++ b/src/EventStore.Client.Streams/StreamSubscription.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;
@@ -9,13 +5,15 @@ namespace EventStore.Client {
///
/// A class representing a .
///
+ [Obsolete]
public class StreamSubscription : IDisposable {
- private readonly IAsyncEnumerable _events;
+ private readonly EventStoreClient.StreamSubscriptionResult _subscription;
+ private readonly IAsyncEnumerator _messages;
private readonly Func _eventAppeared;
private readonly Func _checkpointReached;
private readonly Action? _subscriptionDropped;
private readonly ILogger _log;
- private readonly CancellationTokenSource _disposed;
+ private readonly CancellationTokenSource _cts;
private int _subscriptionDroppedInvoked;
///
@@ -23,58 +21,69 @@ public class StreamSubscription : IDisposable {
///
public string SubscriptionId { get; }
- internal static async Task Confirm(
- IAsyncEnumerable<(SubscriptionConfirmation confirmation, Position?, ResolvedEvent)> read,
+ internal static async Task Confirm(EventStoreClient.StreamSubscriptionResult subscription,
Func eventAppeared,
Action? subscriptionDropped,
ILogger log,
Func? checkpointReached = null,
CancellationToken cancellationToken = default) {
+ var messages = subscription.Messages;
- var enumerator = read.GetAsyncEnumerator(cancellationToken);
- if (await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false) &&
- enumerator.Current.confirmation != SubscriptionConfirmation.None)
- return new StreamSubscription(enumerator, eventAppeared, subscriptionDropped, log,
- checkpointReached, cancellationToken);
- throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed.");
+ var enumerator = messages.GetAsyncEnumerator(cancellationToken);
+ if (!await enumerator.MoveNextAsync().ConfigureAwait(false) ||
+ enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptionId)) {
+ throw new InvalidOperationException($"Subscription to {enumerator} could not be confirmed.");
+ }
+
+ return new StreamSubscription(subscription, enumerator, subscriptionId, eventAppeared, subscriptionDropped,
+ log, checkpointReached, cancellationToken);
}
- private StreamSubscription(
- IAsyncEnumerator<(SubscriptionConfirmation confirmation, Position?, ResolvedEvent)> events,
+ private StreamSubscription(EventStoreClient.StreamSubscriptionResult subscription,
+ IAsyncEnumerator messages, string subscriptionId,
Func eventAppeared,
Action? subscriptionDropped,
ILogger log,
Func? checkpointReached,
CancellationToken cancellationToken = default) {
- _disposed = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- _events = new Enumerable(events, CheckpointReached, _disposed.Token);
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ _subscription = subscription;
+ _messages = messages;
_eventAppeared = eventAppeared;
- _checkpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask);
+ _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask);
_subscriptionDropped = subscriptionDropped;
_log = log;
_subscriptionDroppedInvoked = 0;
- SubscriptionId = events.Current.confirmation.SubscriptionId;
+ SubscriptionId = subscriptionId;
- Task.Run(Subscribe);
- }
+ _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId);
- private Task CheckpointReached(Position position) => _checkpointReached(this, position, _disposed.Token);
+ Task.Run(Subscribe, cancellationToken);
+ }
private async Task Subscribe() {
- _log.LogDebug("Subscription {subscriptionId} confirmed.", SubscriptionId);
- using var _ = _disposed;
+ using var _ = _cts;
try {
- await foreach (var resolvedEvent in _events.ConfigureAwait(false)) {
+ while (await _messages.MoveNextAsync().ConfigureAwait(false)) {
+ var message = _messages.Current;
try {
- _log.LogTrace(
- "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}",
- SubscriptionId,
- resolvedEvent.OriginalEvent.EventStreamId,
- resolvedEvent.OriginalEvent.EventNumber,
- resolvedEvent.OriginalEvent.Position
- );
- await _eventAppeared(this, resolvedEvent, _disposed.Token).ConfigureAwait(false);
+ switch (message) {
+ case StreamMessage.Event(var resolvedEvent):
+ _log.LogTrace(
+ "Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}",
+ SubscriptionId,
+ resolvedEvent.OriginalEvent.EventStreamId,
+ resolvedEvent.OriginalEvent.EventNumber,
+ resolvedEvent.OriginalEvent.Position
+ );
+ await _eventAppeared(this, resolvedEvent, _cts.Token).ConfigureAwait(false);
+ break;
+ case StreamMessage.AllStreamCheckpointReached (var position):
+ await _checkpointReached(this, position, _cts.Token)
+ .ConfigureAwait(false);
+ break;
+ }
} catch (Exception ex) when
(ex is ObjectDisposedException or OperationCanceledException) {
if (_subscriptionDroppedInvoked != 0) {
@@ -104,11 +113,10 @@ private async Task Subscribe() {
} catch (RpcException ex) when (ex.Status.StatusCode == StatusCode.Cancelled &&
ex.Status.Detail.Contains("Call canceled by the client.")) {
_log.LogInformation(
- "Subscription {subscriptionId} was dropped because cancellation was requested by the client.",
+ "Subscription {subscriptionId} was dropped because cancellation was requested by the client.",
SubscriptionId);
- SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex);
- }
- catch (Exception ex) {
+ SubscriptionDropped(SubscriptionDroppedReason.Disposed, ex);
+ } catch (Exception ex) {
if (_subscriptionDroppedInvoked == 0) {
_log.LogError(ex,
"Subscription {subscriptionId} was dropped because an error occurred on the server.",
@@ -129,80 +137,8 @@ private void SubscriptionDropped(SubscriptionDroppedReason reason, Exception? ex
try {
_subscriptionDropped?.Invoke(this, reason, ex);
} finally {
- _disposed.Dispose();
- }
- }
-
- private class Enumerable : IAsyncEnumerable {
- private readonly IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> _inner;
- private readonly Func _checkpointReached;
- private readonly CancellationToken _cancellationToken;
-
- public Enumerable(IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> inner,
- Func checkpointReached, CancellationToken cancellationToken) {
- if (inner == null) {
- throw new ArgumentNullException(nameof(inner));
- }
-
- if (checkpointReached == null) {
- throw new ArgumentNullException(nameof(checkpointReached));
- }
-
- _inner = inner;
- _checkpointReached = checkpointReached;
- _cancellationToken = cancellationToken;
- }
-
- public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
- => new Enumerator(_inner, _checkpointReached, _cancellationToken);
-
- private class Enumerator : IAsyncEnumerator {
- private readonly IAsyncEnumerator<(SubscriptionConfirmation, Position? position, ResolvedEvent
- resolvedEvent)> _inner;
-
- private readonly Func _checkpointReached;
- private readonly CancellationToken _cancellationToken;
-
- public Enumerator(IAsyncEnumerator<(SubscriptionConfirmation, Position?, ResolvedEvent)> inner,
- Func checkpointReached, CancellationToken cancellationToken) {
- if (inner == null) {
- throw new ArgumentNullException(nameof(inner));
- }
-
- if (checkpointReached == null) {
- throw new ArgumentNullException(nameof(checkpointReached));
- }
-
- _inner = inner;
- _checkpointReached = checkpointReached;
- _cancellationToken = cancellationToken;
- }
-
- public ValueTask DisposeAsync() => _inner.DisposeAsync();
-
- public async ValueTask MoveNextAsync() {
- ReadLoop:
- if (_cancellationToken.IsCancellationRequested) {
- return false;
- }
-
- if (!await _inner.MoveNextAsync().ConfigureAwait(false)) {
- return false;
- }
-
- if (_cancellationToken.IsCancellationRequested) {
- return false;
- }
-
- if (!_inner.Current.position.HasValue) {
- return true;
- }
-
- await _checkpointReached(_inner.Current.position.Value).ConfigureAwait(false);
- goto ReadLoop;
- }
-
- public ResolvedEvent Current => _inner.Current.resolvedEvent;
+ _subscription.Dispose();
+ _cts.Dispose();
}
}
}
diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs b/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs
index 67f8153fd..f9e1146c0 100644
--- a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs
+++ b/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs
@@ -9,7 +9,7 @@ public static Options Create(StreamIdentifier streamIdentifier,
StreamRevision expectedStreamRevision, TimeSpan? timeoutAfter) => new() {
StreamIdentifier = streamIdentifier,
StreamPosition = expectedStreamRevision.ToUInt64(),
- Deadline = Timestamp.FromDateTime(timeoutAfter.HasValue
+ Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue
? DateTime.UtcNow + timeoutAfter.Value
: DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc))
};
@@ -24,7 +24,7 @@ public static Options Create(StreamIdentifier streamIdentifier, StreamState expe
_ => ExpectedStreamPositionOneofCase.None
},
expectedStreamPosition_ = new Google.Protobuf.WellKnownTypes.Empty(),
- Deadline = Timestamp.FromDateTime(timeoutAfter.HasValue
+ Deadline21100 = Timestamp.FromDateTime(timeoutAfter.HasValue
? DateTime.UtcNow + timeoutAfter.Value
: DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc))
};
diff --git a/src/EventStore.Client.Streams/SubscriptionConfirmation.cs b/src/EventStore.Client.Streams/SubscriptionConfirmation.cs
deleted file mode 100644
index 368ce7f6b..000000000
--- a/src/EventStore.Client.Streams/SubscriptionConfirmation.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-
-namespace EventStore.Client {
- internal struct SubscriptionConfirmation : IEquatable {
- public static readonly SubscriptionConfirmation None = default;
- public string SubscriptionId { get; }
-
- public SubscriptionConfirmation(string subscriptionId) {
- SubscriptionId = subscriptionId;
- }
-
- public bool Equals(SubscriptionConfirmation other) => SubscriptionId == other.SubscriptionId;
- public override bool Equals(object? obj) => obj is SubscriptionConfirmation other && Equals(other);
- public override int GetHashCode() => SubscriptionId?.GetHashCode() ?? 0;
-
- public static bool operator ==(SubscriptionConfirmation left, SubscriptionConfirmation right) =>
- left.Equals(right);
-
- public static bool operator !=(SubscriptionConfirmation left, SubscriptionConfirmation right) =>
- !left.Equals(right);
-
- public override string ToString() => SubscriptionId;
- }
-}
diff --git a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs b/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs
index 0858fc02d..38469d924 100644
--- a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs
+++ b/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
namespace EventStore.Client {
///
/// A class representing the options to use when filtering read operations.
@@ -21,7 +17,8 @@ public class SubscriptionFilterOptions {
/// A Task invoked and await when a checkpoint is reached.
/// Set the checkpointInterval to define how often this method is called.
///
- public Func CheckpointReached { get; }
+ [Obsolete]
+ public Func CheckpointReached { get; } = null!;
///
///
@@ -33,8 +30,20 @@ public class SubscriptionFilterOptions {
/// Set the checkpointInterval to define how often this method is called.
///
///
- public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1,
- Func? checkpointReached = null) {
+ [Obsolete]
+ public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval,
+ Func? checkpointReached)
+ : this(filter, checkpointInterval) {
+ CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask);
+ }
+
+ ///
+ ///
+ ///
+ /// The to apply.
+ /// Sets how often the checkpointReached callback is called.
+ ///
+ public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval = 1) {
if (checkpointInterval == 0) {
throw new ArgumentOutOfRangeException(nameof(checkpointInterval),
checkpointInterval, $"{nameof(checkpointInterval)} must be greater than 0.");
@@ -42,7 +51,6 @@ public SubscriptionFilterOptions(IEventFilter filter, uint checkpointInterval =
Filter = filter;
CheckpointInterval = checkpointInterval;
- CheckpointReached = checkpointReached ?? ((_, __, ct) => Task.CompletedTask);
}
}
}
diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj
index 3ab3a3faf..ddf36c6af 100644
--- a/src/EventStore.Client/EventStore.Client.csproj
+++ b/src/EventStore.Client/EventStore.Client.csproj
@@ -24,9 +24,13 @@
-
+
+
+
+
+
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs
index 54fa95077..c94429ca3 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs
@@ -10,17 +10,14 @@ public class Issue_1125 : IClassFixture {
[Theory]
[MemberData(nameof(TestCases))]
public async Task persistent_subscription_delivers_all_events(int iteration) {
- if (Environment.OSVersion.IsWindows()) { }
-
- const int eventCount = 250;
+ const int eventCount = 250;
const int totalEvents = eventCount * 2;
- var completed = new TaskCompletionSource();
- var hitCount = 0;
+ var hitCount = 0;
var userCredentials = new UserCredentials("admin", "changeit");
- var streamName = $"stream_{iteration}";
+ var streamName = $"stream_{iteration}";
var subscriptionName = $"subscription_{iteration}";
for (var i = 0; i < eventCount; i++)
@@ -42,48 +39,46 @@ await _fixture.Client.CreateToStreamAsync(
userCredentials: userCredentials
);
- using (await _fixture.Client.SubscribeToStreamAsync(
- streamName,
- subscriptionName,
- async (subscription, @event, retryCount, arg4) => {
- int result;
- if (retryCount == 0 || retryCount is null) {
- result = Interlocked.Increment(ref hitCount);
-
- await subscription.Ack(@event);
-
- if (totalEvents == result)
- completed.TrySetResult(true);
- }
- else {
- // This is a retry
- await subscription.Ack(@event);
- }
- },
- (s, dr, e) => {
- if (e != null)
- completed.TrySetException(e);
- else
- completed.TrySetException(new Exception($"{dr}"));
- },
- userCredentials
- )
- ) {
+ await using var subscription =
+ _fixture.Client.SubscribeToStream(streamName, subscriptionName, userCredentials: userCredentials);
+
+ await Task.WhenAll(Subscribe(), Append()).WithTimeout();
+
+ Assert.Equal(totalEvents, hitCount);
+
+ return;
+
+ async Task Subscribe() {
+ await foreach (var message in subscription.Messages) {
+ if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) {
+ continue;
+ }
+
+ if (retryCount is 0 or null) {
+ var result = Interlocked.Increment(ref hitCount);
+
+ await subscription.Ack(resolvedEvent);
+
+ if (totalEvents == result)
+ return;
+ } else {
+ // This is a retry
+ await subscription.Ack(resolvedEvent);
+ }
+ }
+ }
+
+ async Task Append() {
for (var i = 0; i < eventCount; i++)
await _fixture.StreamsClient.AppendToStreamAsync(
streamName,
StreamState.Any,
- _fixture.CreateTestEvents()
- );
-
- await completed.Task.WithTimeout(TimeSpan.FromSeconds(30));
+ _fixture.CreateTestEvents());
}
-
- Assert.Equal(totalEvents, hitCount);
}
public class Fixture : EventStoreClientFixture {
protected override Task Given() => Task.CompletedTask;
- protected override Task When() => Task.CompletedTask;
+ protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs
index d21d7061d..db2818abf 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs
@@ -1,42 +1,31 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class connect_to_existing_with_max_one_client : IClassFixture {
- const string Group = "maxoneclient";
+ private const string Group = "maxoneclient";
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task the_second_subscription_fails_to_connect() {
- using var first = await _fixture.Client.SubscribeToAllAsync(
- Group,
- delegate { return Task.CompletedTask; },
- userCredentials: TestCredentials.Root
- ).WithTimeout();
-
+ public async Task the_second_subscription_fails_to_connect2() {
var ex = await Assert.ThrowsAsync(
- async () => {
- using var _ = await _fixture.Client.SubscribeToAllAsync(
- Group,
- delegate { return Task.CompletedTask; },
- userCredentials: TestCredentials.Root
- );
- }
- ).WithTimeout();
+ () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout()));
Assert.Equal(SystemStreams.AllStream, ex.StreamName);
Assert.Equal(Group, ex.GroupName);
+ return;
+
+ async Task Subscribe() {
+ await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+ await subscription.Messages.AnyAsync();
+ }
}
public class Fixture : EventStoreClientFixture {
protected override Task Given() =>
- Client.CreateToAllAsync(
- Group,
- new(maxSubscriberCount: 1),
- userCredentials: TestCredentials.Root
- );
+ Client.CreateToAllAsync(Group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root);
protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs
new file mode 100644
index 000000000..200e12bb3
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client_obsolete.cs
@@ -0,0 +1,43 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_max_one_client_obsolete : IClassFixture {
+ const string Group = "maxoneclient";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_second_subscription_fails_to_connect() {
+ using var first = await _fixture.Client.SubscribeToAllAsync(
+ Group,
+ delegate { return Task.CompletedTask; },
+ userCredentials: TestCredentials.Root
+ ).WithTimeout();
+
+ var ex = await Assert.ThrowsAsync(
+ async () => {
+ using var _ = await _fixture.Client.SubscribeToAllAsync(
+ Group,
+ delegate { return Task.CompletedTask; },
+ userCredentials: TestCredentials.Root
+ );
+ }
+ ).WithTimeout();
+
+ Assert.Equal(SystemStreams.AllStream, ex.StreamName);
+ Assert.Equal(Group, ex.GroupName);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ protected override Task Given() =>
+ Client.CreateToAllAsync(
+ Group,
+ new(maxSubscriberCount: 1),
+ userCredentials: TestCredentials.Root
+ );
+
+ protected override Task When() => Task.CompletedTask;
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs
index 2cee251f5..e10ed9909 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs
@@ -1,36 +1,23 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_with_permissions
- : IClassFixture {
- const string Group = "connectwithpermissions";
-
- readonly Fixture _fixture;
+public class connect_to_existing_with_permissions : IClassFixture {
+ private const string Group = "connectwithpermissions";
+ private readonly Fixture _fixture;
public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_succeeds() {
- var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>();
- using var subscription = await _fixture.Client.SubscribeToAllAsync(
- Group,
- delegate { return Task.CompletedTask; },
- (s, reason, ex) => dropped.TrySetResult((reason, ex)),
- TestCredentials.Root
- ).WithTimeout();
-
- Assert.NotNull(subscription);
+ await using var subscription =
+ _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
- await Assert.ThrowsAsync(() => dropped.Task.WithTimeout());
+ Assert.True(await subscription.Messages
+ .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation);
}
public class Fixture : EventStoreClientFixture {
- protected override Task Given() =>
- Client.CreateToAllAsync(
- Group,
- new(),
- userCredentials: TestCredentials.Root
- );
+ protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root);
protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs
new file mode 100644
index 000000000..719dc8b02
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions_obsolete.cs
@@ -0,0 +1,37 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_permissions_obsolete
+ : IClassFixture {
+ const string Group = "connectwithpermissions";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_succeeds() {
+ var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>();
+ using var subscription = await _fixture.Client.SubscribeToAllAsync(
+ Group,
+ delegate { return Task.CompletedTask; },
+ (s, reason, ex) => dropped.TrySetResult((reason, ex)),
+ TestCredentials.Root
+ ).WithTimeout();
+
+ Assert.NotNull(subscription);
+
+ await Assert.ThrowsAsync(() => dropped.Task.WithTimeout());
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ protected override Task Given() =>
+ Client.CreateToAllAsync(
+ Group,
+ new(),
+ userCredentials: TestCredentials.Root
+ );
+
+ protected override Task When() => Task.CompletedTask;
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs
index e28875425..47e36c620 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs
@@ -1,65 +1,52 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_with_start_from_beginning : IClassFixture {
- const string Group = "startfrombeginning";
-
- readonly Fixture _fixture;
+public class connect_to_existing_with_start_from_beginning
+ : IClassFixture {
+ private const string Group = "startfrombeginning";
+ private readonly Fixture _fixture;
public connect_to_existing_with_start_from_beginning(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_gets_event_zero_as_its_first_event() {
- var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10));
- Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId);
+ var resolvedEvent = await _fixture.Subscription!.Messages.OfType()
+ .Select(e => e.ResolvedEvent)
+ .FirstOrDefaultAsync().AsTask().WithTimeout();
+
+ Assert.Equal(_fixture.Events[0].Event.EventId, resolvedEvent.Event.EventId);
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstEventSource;
-
- PersistentSubscription? _subscription;
-
- public Fixture() => _firstEventSource = new();
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
- public ResolvedEvent[]? Events { get; set; }
-
- public Task FirstEvent => _firstEventSource.Task;
+ public ResolvedEvent[] Events { get; private set; } = Array.Empty();
protected override async Task Given() {
//append 10 events to random streams to make sure we have at least 10 events in the transaction file
- foreach (var @event in CreateTestEvents(10))
- await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event });
+ foreach (var @event in CreateTestEvents(10)) {
+ await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream,
+ new[] { @event });
+ }
+
+ Events = await StreamsClient
+ .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root)
+ .ToArrayAsync();
+
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root);
+ }
- Events = await StreamsClient.ReadAllAsync(
- Direction.Forwards,
- Position.Start,
- 10,
- userCredentials: TestCredentials.Root
- ).ToArrayAsync();
+ protected override Task When() {
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: Position.Start),
- userCredentials: TestCredentials.Root
- );
+ return Task.CompletedTask;
}
- protected override async Task When() =>
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- _firstEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs
new file mode 100644
index 000000000..331930474
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning_obsolete.cs
@@ -0,0 +1,66 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_beginning_obsolete : IClassFixture {
+ const string Group = "startfrombeginning";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_beginning_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_event_zero_as_its_first_event() {
+ var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10));
+ Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstEventSource;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _firstEventSource = new();
+
+ public ResolvedEvent[]? Events { get; set; }
+
+ public Task FirstEvent => _firstEventSource.Task;
+
+ protected override async Task Given() {
+ //append 10 events to random streams to make sure we have at least 10 events in the transaction file
+ foreach (var @event in CreateTestEvents(10))
+ await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event });
+
+ Events = await StreamsClient.ReadAllAsync(
+ Direction.Forwards,
+ Position.Start,
+ 10,
+ userCredentials: TestCredentials.Root
+ ).ToArrayAsync();
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: Position.Start),
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() =>
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ _firstEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs
index d25d8a9bd..17a28d1d6 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs
@@ -1,62 +1,44 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_with_start_from_not_set : IClassFixture {
- const string Group = "startfromend1";
- readonly Fixture _fixture;
+public class connect_to_existing_with_start_from_not_set
+ : IClassFixture {
+ private const string Group = "startfromend1";
+ private readonly Fixture _fixture;
public connect_to_existing_with_start_from_not_set(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task the_subscription_gets_no_non_system_events() =>
- await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout());
+ public async Task the_subscription_gets_no_non_system_events() {
+ await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages
+ .OfType()
+ .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId))
+ .AnyAsync()
+ .AsTask()
+ .WithTimeout(TimeSpan.FromMilliseconds(250)));
+ }
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstNonSystemEventSource;
- PersistentSubscription? _subscription;
-
- public Fixture() => _firstNonSystemEventSource = new();
-
- public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
protected override async Task Given() {
foreach (var @event in CreateTestEvents(10))
- await StreamsClient.AppendToStreamAsync(
- "non-system-stream-" + Guid.NewGuid(),
- StreamState.Any,
- new[] {
- @event
- }
- );
-
- await Client.CreateToAllAsync(
- Group,
- new(),
- userCredentials: TestCredentials.Root
- );
+ await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any,
+ new[] { @event });
+
+ await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root);
}
- protected override async Task When() =>
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
- await subscription.Ack(e);
- return;
- }
-
- _firstNonSystemEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstNonSystemEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
-
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ protected override Task When() {
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+ return Task.CompletedTask;
+ }
+
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs
new file mode 100644
index 000000000..2dce2c8dd
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_obsolete.cs
@@ -0,0 +1,64 @@
+
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_not_set_obsolete : IClassFixture {
+ const string Group = "startfromend1";
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_not_set_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_no_non_system_events() =>
+ await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout());
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstNonSystemEventSource;
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _firstNonSystemEventSource = new();
+
+ public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+
+ protected override async Task Given() {
+ foreach (var @event in CreateTestEvents(10))
+ await StreamsClient.AppendToStreamAsync(
+ "non-system-stream-" + Guid.NewGuid(),
+ StreamState.Any,
+ new[] {
+ @event
+ }
+ );
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(),
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() =>
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
+ await subscription.Ack(e);
+ return;
+ }
+
+ _firstNonSystemEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstNonSystemEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs
index c0f70ddb4..bbd8ef16e 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs
@@ -2,70 +2,53 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class connect_to_existing_with_start_from_not_set_then_event_written
: IClassFixture {
- const string Group = "startfromnotset2";
+ private const string Group = "startfromnotset2";
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
- public
- connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) =>
- _fixture = fixture;
+ public connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() {
- var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout();
- Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId);
+ var resolvedEvent = await _fixture.Subscription!.Messages.OfType()
+ .Select(e => e.ResolvedEvent)
+ .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId))
+ .FirstOrDefaultAsync().AsTask().WithTimeout();
+
+ Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId);
Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId);
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstNonSystemEventSource;
-
- public readonly EventData? ExpectedEvent;
- public readonly string ExpectedStreamId;
+ public readonly EventData ExpectedEvent;
+ public readonly string ExpectedStreamId;
- PersistentSubscription? _subscription;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
public Fixture() {
- _firstNonSystemEventSource = new();
- ExpectedEvent = CreateTestEvents(1).First();
- ExpectedStreamId = Guid.NewGuid().ToString();
+ ExpectedEvent = CreateTestEvents(1).First();
+ ExpectedStreamId = Guid.NewGuid().ToString();
}
- public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
-
protected override async Task Given() {
- foreach (var @event in CreateTestEvents(10))
- await StreamsClient.AppendToStreamAsync(
- "non-system-stream-" + Guid.NewGuid(),
- StreamState.Any,
- new[] { @event }
- );
+ foreach (var @event in CreateTestEvents(10)) {
+ await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any,
+ new[] { @event });
+ }
await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root);
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
- await subscription.Ack(e);
- return;
- }
-
- _firstNonSystemEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstNonSystemEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
}
- protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! });
+ protected override async Task When() =>
+ await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent });
+
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs
new file mode 100644
index 000000000..95dc8edf6
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs
@@ -0,0 +1,72 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_not_set_then_event_written_obsolete
+ : IClassFixture {
+ const string Group = "startfromnotset2";
+
+ readonly Fixture _fixture;
+
+ public
+ connect_to_existing_with_start_from_not_set_then_event_written_obsolete(Fixture fixture) =>
+ _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event2() {
+ var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout();
+ Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId);
+ Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstNonSystemEventSource;
+
+ public readonly EventData? ExpectedEvent;
+ public readonly string ExpectedStreamId;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() {
+ _firstNonSystemEventSource = new();
+ ExpectedEvent = CreateTestEvents(1).First();
+ ExpectedStreamId = Guid.NewGuid().ToString();
+ }
+
+ public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+
+ protected override async Task Given() {
+ foreach (var @event in CreateTestEvents(10))
+ await StreamsClient.AppendToStreamAsync(
+ "non-system-stream-" + Guid.NewGuid(),
+ StreamState.Any,
+ new[] { @event }
+ );
+
+ await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root);
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
+ await subscription.Ack(e);
+ return;
+ }
+
+ _firstNonSystemEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstNonSystemEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! });
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs
index bc1632630..e7f3c9913 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs
@@ -1,62 +1,46 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_with_start_from_set_to_end_position : IClassFixture {
- const string Group = "startfromend1";
+public class connect_to_existing_with_start_from_set_to_end_position
+ : IClassFixture {
+ private const string Group = "startfromend1";
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public connect_to_existing_with_start_from_set_to_end_position(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task the_subscription_gets_no_non_system_events() =>
- await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout());
+ public async Task the_subscription_gets_no_non_system_events() {
+ await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages
+ .OfType()
+ .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId))
+ .AnyAsync()
+ .AsTask()
+ .WithTimeout(TimeSpan.FromMilliseconds(250)));
+ }
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstNonSystemEventSource;
-
- PersistentSubscription? _subscription;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
- public Fixture() => _firstNonSystemEventSource = new();
+ protected override async Task Given() {
+ foreach (var @event in CreateTestEvents(10)) {
+ await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any,
+ new[] { @event });
+ }
- public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root);
+ }
- protected override async Task Given() {
- foreach (var @event in CreateTestEvents(10))
- await StreamsClient.AppendToStreamAsync(
- "non-system-stream-" + Guid.NewGuid(),
- StreamState.Any,
- new[] { @event }
- );
-
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: Position.End),
- userCredentials: TestCredentials.Root
- );
+ protected override Task When() {
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+ return Task.CompletedTask;
}
- protected override async Task When() =>
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
- await subscription.Ack(e);
- return;
- }
-
- _firstNonSystemEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstNonSystemEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
-
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs
new file mode 100644
index 000000000..5b82b063d
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs
@@ -0,0 +1,63 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_set_to_end_position_obsolete: IClassFixture {
+ const string Group = "startfromend1";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_set_to_end_position_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_no_non_system_events() =>
+ await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout());
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstNonSystemEventSource;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _firstNonSystemEventSource = new();
+
+ public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+
+ protected override async Task Given() {
+ foreach (var @event in CreateTestEvents(10))
+ await StreamsClient.AppendToStreamAsync(
+ "non-system-stream-" + Guid.NewGuid(),
+ StreamState.Any,
+ new[] { @event }
+ );
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: Position.End),
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() =>
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
+ await subscription.Ack(e);
+ return;
+ }
+
+ _firstNonSystemEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstNonSystemEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs
index 9fee746b8..835c8538c 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs
@@ -3,66 +3,54 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class
connect_to_existing_with_start_from_set_to_end_position_then_event_written
: IClassFixture {
- const string Group = "startfromnotset2";
+ private const string Group = "startfromnotset2";
+ private readonly Fixture _fixture;
- readonly Fixture _fixture;
-
- public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) => _fixture = fixture;
+ public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) =>
+ _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() {
- var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout();
+ var resolvedEvent = await _fixture.Subscription!.Messages
+ .OfType()
+ .Select(e => e.ResolvedEvent)
+ .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId))
+ .FirstAsync()
+ .AsTask()
+ .WithTimeout();
Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId);
Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId);
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstNonSystemEventSource;
- public readonly EventData ExpectedEvent;
- public readonly string ExpectedStreamId;
- PersistentSubscription? _subscription;
+ public readonly EventData ExpectedEvent;
+ public readonly string ExpectedStreamId;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
public Fixture() {
- _firstNonSystemEventSource = new();
- ExpectedEvent = CreateTestEvents(1).First();
- ExpectedStreamId = Guid.NewGuid().ToString();
+ ExpectedEvent = CreateTestEvents(1).First();
+ ExpectedStreamId = Guid.NewGuid().ToString();
}
- public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
-
protected override async Task Given() {
- foreach (var @event in CreateTestEvents(10))
- await StreamsClient.AppendToStreamAsync(
- "non-system-stream-" + Guid.NewGuid(),
- StreamState.Any,
- new[] { @event }
- );
+ foreach (var @event in CreateTestEvents(10)) {
+ await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any,
+ new[] { @event });
+ }
await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root);
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
- await subscription.Ack(e);
- return;
- }
-
- _firstNonSystemEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstNonSystemEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
}
- protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent });
+ protected override async Task When() =>
+ await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent });
+
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs
new file mode 100644
index 000000000..4f9dea90e
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs
@@ -0,0 +1,69 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class
+ connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete
+ : IClassFixture {
+ const string Group = "startfromnotset2";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() {
+ var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout();
+ Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId);
+ Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstNonSystemEventSource;
+ public readonly EventData ExpectedEvent;
+ public readonly string ExpectedStreamId;
+ PersistentSubscription? _subscription;
+
+ public Fixture() {
+ _firstNonSystemEventSource = new();
+ ExpectedEvent = CreateTestEvents(1).First();
+ ExpectedStreamId = Guid.NewGuid().ToString();
+ }
+
+ public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task;
+
+ protected override async Task Given() {
+ foreach (var @event in CreateTestEvents(10))
+ await StreamsClient.AppendToStreamAsync(
+ "non-system-stream-" + Guid.NewGuid(),
+ StreamState.Any,
+ new[] { @event }
+ );
+
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root);
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ if (SystemStreams.IsSystemStream(e.OriginalStreamId)) {
+ await subscription.Ack(e);
+ return;
+ }
+
+ _firstNonSystemEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstNonSystemEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent });
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs
index 5c0463a02..b45330a9e 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs
@@ -2,47 +2,47 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class connect_to_existing_with_start_from_set_to_invalid_middle_position
: IClassFixture {
- const string Group = "startfrominvalid1";
- readonly Fixture _fixture;
+ private const string Group = "startfrominvalid1";
+ private readonly Fixture _fixture;
public connect_to_existing_with_start_from_set_to_invalid_middle_position(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_is_dropped() {
- var (reason, exception) = await _fixture.Dropped.WithTimeout();
- Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
- Assert.IsType(exception);
+ var ex = await Assert.ThrowsAsync(async () =>
+ await _fixture.Enumerator!.MoveNextAsync());
+
+ Assert.Equal(SystemStreams.AllStream, ex.StreamName);
+ Assert.Equal(Group, ex.GroupName);
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped;
-
- PersistentSubscription? _subscription;
-
- public Fixture() => _dropped = new();
-
- public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
+ public IAsyncEnumerator? Enumerator { get; private set; }
protected override async Task Given() {
var invalidPosition = new Position(1L, 1L);
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: invalidPosition),
- userCredentials: TestCredentials.Root
- );
+ await Client.CreateToAllAsync(Group, new(startFrom: invalidPosition),
+ userCredentials: TestCredentials.Root);
}
- protected override async Task When() =>
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => await subscription.Ack(e),
- (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); },
- TestCredentials.Root
- );
-
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ protected override async Task When() {
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+ Enumerator = Subscription.Messages.GetAsyncEnumerator();
+
+ await Enumerator.MoveNextAsync();
+ }
+
+ public override async Task DisposeAsync() {
+ if (Enumerator is not null) {
+ await Enumerator.DisposeAsync();
+ }
+
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs
new file mode 100644
index 000000000..639164294
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs
@@ -0,0 +1,49 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete
+ : IClassFixture {
+ const string Group = "startfrominvalid1";
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_is_dropped() {
+ var (reason, exception) = await _fixture.Dropped.WithTimeout();
+ Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
+ Assert.IsType(exception);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _dropped = new();
+
+ public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task;
+
+ protected override async Task Given() {
+ var invalidPosition = new Position(1L, 1L);
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: invalidPosition),
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() =>
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => await subscription.Ack(e),
+ (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); },
+ TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs
index 9469af0af..daaf838a9 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs
@@ -2,63 +2,50 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class connect_to_existing_with_start_from_set_to_valid_middle_position
: IClassFixture {
- const string Group = "startfromvalid";
+ private const string Group = "startfromvalid";
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public connect_to_existing_with_start_from_set_to_valid_middle_position(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() {
- var resolvedEvent = await _fixture.FirstEvent.WithTimeout();
+ var resolvedEvent = await _fixture.Subscription!.Messages.OfType()
+ .Select(e => e.ResolvedEvent)
+ .FirstAsync()
+ .AsTask()
+ .WithTimeout();
Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position);
Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId);
Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId);
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _firstEventSource;
- PersistentSubscription? _subscription;
-
- public Fixture() => _firstEventSource = new();
-
- public Task FirstEvent => _firstEventSource.Task;
- public ResolvedEvent ExpectedEvent { get; private set; }
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
+ public ResolvedEvent ExpectedEvent { get; private set; }
protected override async Task Given() {
- var events = await StreamsClient.ReadAllAsync(
- Direction.Forwards,
- Position.Start,
- 10,
- userCredentials: TestCredentials.Root
- ).ToArrayAsync();
+ var events = await StreamsClient
+ .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root)
+ .ToArrayAsync();
ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: ExpectedEvent.OriginalPosition),
- userCredentials: TestCredentials.Root
- );
+ await Client.CreateToAllAsync(Group, new(startFrom: ExpectedEvent.OriginalPosition),
+ userCredentials: TestCredentials.Root);
+ }
+
+ protected override Task When() {
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+ return Task.CompletedTask;
}
- protected override async Task When() =>
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- _firstEventSource.TrySetResult(e);
- await subscription.Ack(e);
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _firstEventSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs
new file mode 100644
index 000000000..7fc7b07c8
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs
@@ -0,0 +1,65 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete
+ : IClassFixture {
+ const string Group = "startfromvalid";
+
+ readonly Fixture _fixture;
+
+ public connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() {
+ var resolvedEvent = await _fixture.FirstEvent.WithTimeout();
+ Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position);
+ Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId);
+ Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _firstEventSource;
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _firstEventSource = new();
+
+ public Task FirstEvent => _firstEventSource.Task;
+ public ResolvedEvent ExpectedEvent { get; private set; }
+
+ protected override async Task Given() {
+ var events = await StreamsClient.ReadAllAsync(
+ Direction.Forwards,
+ Position.Start,
+ 10,
+ userCredentials: TestCredentials.Root
+ ).ToArrayAsync();
+
+ ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: ExpectedEvent.OriginalPosition),
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() =>
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ _firstEventSource.TrySetResult(e);
+ await subscription.Ack(e);
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _firstEventSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs
index 8f18f498f..865281ec0 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs
@@ -1,31 +1,23 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_without_permissions
- : IClassFixture {
- readonly Fixture _fixture;
+public class connect_to_existing_without_permissions : IClassFixture {
+ private readonly Fixture _fixture;
public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public Task throws_access_denied() =>
Assert.ThrowsAsync(
async () => {
- using var _ = await _fixture.Client.SubscribeToAllAsync(
- "agroupname55",
- delegate { return Task.CompletedTask; }
- );
- }
- ).WithTimeout();
+ await using var subscription = _fixture.Client.SubscribeToAll("agroupname55");
+ await subscription.AnyAsync().AsTask().WithTimeout();
+ });
public class Fixture : EventStoreClientFixture {
public Fixture() : base(noDefaultCredentials: true) { }
protected override Task Given() =>
- Client.CreateToAllAsync(
- "agroupname55",
- new(),
- userCredentials: TestCredentials.Root
- );
+ Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root);
protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs
new file mode 100644
index 000000000..82ee362b8
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions_obsolete.cs
@@ -0,0 +1,32 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_without_permissions_obsolete
+ : IClassFixture {
+ readonly Fixture _fixture;
+ public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public Task throws_access_denied() =>
+ Assert.ThrowsAsync(
+ async () => {
+ using var _ = await _fixture.Client.SubscribeToAllAsync(
+ "agroupname55",
+ delegate { return Task.CompletedTask; }
+ );
+ }
+ ).WithTimeout();
+
+ public class Fixture : EventStoreClientFixture {
+ public Fixture() : base(noDefaultCredentials: true) { }
+
+ protected override Task Given() =>
+ Client.CreateToAllAsync(
+ "agroupname55",
+ new(),
+ userCredentials: TestCredentials.Root
+ );
+
+ protected override Task When() => Task.CompletedTask;
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs
index 7499264d9..56eccd0fe 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs
@@ -1,32 +1,25 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_to_existing_without_read_all_permissions
+public class connect_to_existing_without_read_all_permissions
: IClassFixture {
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public connect_to_existing_without_read_all_permissions(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public Task throws_access_denied() =>
Assert.ThrowsAsync(
async () => {
- using var _ = await _fixture.Client.SubscribeToAllAsync(
- "agroupname55",
- delegate { return Task.CompletedTask; },
- userCredentials: TestCredentials.TestUser1
- );
- }
- ).WithTimeout();
+ await using var subscription =
+ _fixture.Client.SubscribeToAll("agroupname55", userCredentials: TestCredentials.TestUser1);
+ await subscription.Messages.AnyAsync().AsTask().WithTimeout();
+ });
public class Fixture : EventStoreClientFixture {
public Fixture() : base(noDefaultCredentials: true) { }
protected override Task Given() =>
- Client.CreateToAllAsync(
- "agroupname55",
- new(),
- userCredentials: TestCredentials.Root
- );
+ Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root);
protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs
new file mode 100644
index 000000000..0ffe8daf4
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions_obsolete.cs
@@ -0,0 +1,33 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_existing_without_read_all_permissions_obsolete
+ : IClassFixture {
+ readonly Fixture _fixture;
+ public connect_to_existing_without_read_all_permissions_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public Task throws_access_denied() =>
+ Assert.ThrowsAsync(
+ async () => {
+ using var _ = await _fixture.Client.SubscribeToAllAsync(
+ "agroupname55",
+ delegate { return Task.CompletedTask; },
+ userCredentials: TestCredentials.TestUser1
+ );
+ }
+ ).WithTimeout();
+
+ public class Fixture : EventStoreClientFixture {
+ public Fixture() : base(noDefaultCredentials: true) { }
+
+ protected override Task Given() =>
+ Client.CreateToAllAsync(
+ "agroupname55",
+ new(),
+ userCredentials: TestCredentials.Root
+ );
+
+ protected override Task When() => Task.CompletedTask;
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs
index 9b452a46f..40a541040 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs
@@ -2,30 +2,23 @@ namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class connect_to_non_existing_with_permissions
: IClassFixture {
- const string Group = "foo";
+ private const string Group = "foo";
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
public async Task throws_persistent_subscription_not_found() {
- var ex = await Assert.ThrowsAsync(
- async () => {
- using var _ = await _fixture.Client.SubscribeToAllAsync(
- Group,
- delegate { return Task.CompletedTask; },
- userCredentials: TestCredentials.Root
- );
- }
- ).WithTimeout();
+ await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
- Assert.Equal(SystemStreams.AllStream, ex.StreamName);
- Assert.Equal(Group, ex.GroupName);
+ Assert.True(await subscription.Messages.OfType().AnyAsync()
+ .AsTask()
+ .WithTimeout());
}
public class Fixture : EventStoreClientFixture {
protected override Task Given() => Task.CompletedTask;
- protected override Task When() => Task.CompletedTask;
+ protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs
new file mode 100644
index 000000000..06f9a6d68
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions_obsolete.cs
@@ -0,0 +1,32 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_to_non_existing_with_permissions_obsolete
+ : IClassFixture {
+ const string Group = "foo";
+
+ readonly Fixture _fixture;
+
+ public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task throws_persistent_subscription_not_found() {
+ var ex = await Assert.ThrowsAsync(
+ async () => {
+ using var _ = await _fixture.Client.SubscribeToAllAsync(
+ Group,
+ delegate { return Task.CompletedTask; },
+ userCredentials: TestCredentials.Root
+ );
+ }
+ ).WithTimeout();
+
+ Assert.Equal(SystemStreams.AllStream, ex.StreamName);
+ Assert.Equal(Group, ex.GroupName);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ protected override Task Given() => Task.CompletedTask;
+ protected override Task When() => Task.CompletedTask;
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs
index 7fc26350e..ea4236278 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs
@@ -1,58 +1,49 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class connect_with_retries
- : IClassFixture {
- const string Group = "retries";
- readonly Fixture _fixture;
+public class connect_with_retries : IClassFixture {
+ private const string Group = "retries";
+ private readonly Fixture _fixture;
public connect_with_retries(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout());
+ public async Task events_are_retried_until_success() {
+ var retryCount = await _fixture.Subscription!.Messages.OfType()
+ .SelectAwait(async e => {
+ if (e.RetryCount > 4) {
+ await _fixture.Subscription.Ack(e.ResolvedEvent);
+ } else {
+ await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry,
+ "Not yet tried enough times", e.ResolvedEvent);
+ }
+
+ return e.RetryCount;
+ })
+ .Where(retryCount => retryCount > 4)
+ .FirstOrDefaultAsync()
+ .AsTask()
+ .WithTimeout();
+
+ Assert.Equal(5, retryCount);
+ }
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource _retryCountSource;
- PersistentSubscription? _subscription;
-
- public Fixture() => _retryCountSource = new();
-
- public Task RetryCount => _retryCountSource.Task;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
protected override async Task Given() {
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: Position.Start),
- userCredentials: TestCredentials.Root
- );
-
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, r, ct) => {
- if (r > 4) {
- _retryCountSource.TrySetResult(r.Value);
- await subscription.Ack(e.Event.EventId);
- }
- else {
- await subscription.Nack(
- PersistentSubscriptionNakEventAction.Retry,
- "Not yet tried enough times",
- e
- );
- }
- },
- (subscription, reason, ex) => {
- if (reason != SubscriptionDroppedReason.Disposed)
- _retryCountSource.TrySetException(ex!);
- },
- TestCredentials.Root
- );
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root);
+
+ Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
}
protected override Task When() => Task.CompletedTask;
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs
new file mode 100644
index 000000000..0eff7957e
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries_obsolete.cs
@@ -0,0 +1,59 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class connect_with_retries_obsolete
+ : IClassFixture {
+ const string Group = "retries";
+ readonly Fixture _fixture;
+
+ public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout());
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource _retryCountSource;
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _retryCountSource = new();
+
+ public Task RetryCount => _retryCountSource.Task;
+
+ protected override async Task Given() {
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: Position.Start),
+ userCredentials: TestCredentials.Root
+ );
+
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, r, ct) => {
+ if (r > 4) {
+ _retryCountSource.TrySetResult(r.Value);
+ await subscription.Ack(e.Event.EventId);
+ }
+ else {
+ await subscription.Nack(
+ PersistentSubscriptionNakEventAction.Retry,
+ "Not yet tried enough times",
+ e
+ );
+ }
+ },
+ (subscription, reason, ex) => {
+ if (reason != SubscriptionDroppedReason.Disposed)
+ _retryCountSource.TrySetException(ex!);
+ },
+ TestCredentials.Root
+ );
+ }
+
+ protected override Task When() => Task.CompletedTask;
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs
index 4403087e6..4db83e251 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs
@@ -1,67 +1,23 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class deleting_existing_with_subscriber
- : IClassFixture {
- readonly Fixture _fixture;
+public class deleting_existing_with_subscriber : IClassFixture {
+ public const string Group = "groupname123";
+ private readonly Fixture _fixture;
public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task the_subscription_is_dropped() {
- var (reason, exception) = await _fixture.Dropped.WithTimeout();
- Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
- var ex = Assert.IsType(exception);
-
-#if NET
- Assert.Equal(SystemStreams.AllStream, ex.StreamName);
- Assert.Equal("groupname123", ex.GroupName);
-#endif
- }
-
- [Fact(Skip = "Isn't this how it should work?")]
public async Task the_subscription_is_dropped_with_not_found() {
- var (reason, exception) = await _fixture.Dropped.WithTimeout();
- Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
- var ex = Assert.IsType(exception);
- Assert.Equal(SystemStreams.AllStream, ex.StreamName);
- Assert.Equal("groupname123", ex.GroupName);
+ await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root);
+
+ Assert.True(await subscription.Messages.OfType().AnyAsync()
+ .AsTask()
+ .WithTimeout());
}
public class Fixture : EventStoreClientFixture {
- readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped;
- PersistentSubscription? _subscription;
-
- public Fixture() => _dropped = new();
-
- public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task;
-
- protected override async Task Given() {
- await Client.CreateToAllAsync(
- "groupname123",
- new(),
- userCredentials: TestCredentials.Root
- );
-
- _subscription = await Client.SubscribeToAllAsync(
- "groupname123",
- async (s, e, i, ct) => await s.Ack(e),
- (s, r, e) => _dropped.TrySetResult((r, e)),
- TestCredentials.Root
- );
-
- // todo: investigate why this test is flaky without this delay
- await Task.Delay(500);
- }
-
- protected override Task When() =>
- Client.DeleteToAllAsync(
- "groupname123",
- userCredentials: TestCredentials.Root
- );
+ protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root);
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
- }
+ protected override Task When() => Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root);
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs
new file mode 100644
index 000000000..556224f23
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber_obsolete.cs
@@ -0,0 +1,66 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class deleting_existing_with_subscriber_obsolete
+ : IClassFixture {
+ readonly Fixture _fixture;
+
+ public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task the_subscription_is_dropped() {
+ var (reason, exception) = await _fixture.Dropped.WithTimeout();
+ Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
+ var ex = Assert.IsType(exception);
+
+ Assert.Equal(SystemStreams.AllStream, ex.StreamName);
+ Assert.Equal("groupname123", ex.GroupName);
+ }
+
+ [Fact(Skip = "Isn't this how it should work?")]
+ public async Task the_subscription_is_dropped_with_not_found() {
+ var (reason, exception) = await _fixture.Dropped.WithTimeout();
+ Assert.Equal(SubscriptionDroppedReason.ServerError, reason);
+ var ex = Assert.IsType(exception);
+ Assert.Equal(SystemStreams.AllStream, ex.StreamName);
+ Assert.Equal("groupname123", ex.GroupName);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped;
+ PersistentSubscription? _subscription;
+
+ public Fixture() => _dropped = new();
+
+ public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task;
+
+ protected override async Task Given() {
+ await Client.CreateToAllAsync(
+ "groupname123",
+ new(),
+ userCredentials: TestCredentials.Root
+ );
+
+ _subscription = await Client.SubscribeToAllAsync(
+ "groupname123",
+ async (s, e, i, ct) => await s.Ack(e),
+ (s, r, e) => _dropped.TrySetResult((r, e)),
+ TestCredentials.Root
+ );
+
+ // todo: investigate why this test is flaky without this delay
+ await Task.Delay(500);
+ }
+
+ protected override Task When() =>
+ Client.DeleteToAllAsync(
+ "groupname123",
+ userCredentials: TestCredentials.Root
+ );
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs
index ca3afffd6..ab748a5d5 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs
@@ -1,9 +1,9 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class get_info : IClassFixture {
- const string GroupName = nameof(get_info);
+ private const string GroupName = nameof(get_info);
- static readonly PersistentSubscriptionSettings Settings = new(
+ private static readonly PersistentSubscriptionSettings Settings = new(
resolveLinkTos: true,
startFrom: Position.Start,
extraStatistics: true,
@@ -16,19 +16,18 @@ public class get_info : IClassFixture {
checkPointLowerBound: 1,
checkPointUpperBound: 1,
maxSubscriberCount: 500,
- consumerStrategyName: SystemConsumerStrategies.Pinned
- );
+ consumerStrategyName: SystemConsumerStrategies.Pinned);
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public get_info(Fixture fixture) => _fixture = fixture;
[Fact]
public async Task throws_when_not_supported() {
- if (SupportsPSToAll.No)
- await Assert.ThrowsAsync(
- async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); }
- );
+ if (SupportsPSToAll.No) {
+ await Assert.ThrowsAsync(async () =>
+ await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root));
+ }
}
[SupportsPSToAll.Fact]
@@ -100,56 +99,45 @@ public async Task returns_expected_result() {
[SupportsPSToAll.Fact]
public async Task throws_with_non_existing_subscription() =>
await Assert.ThrowsAsync(
- async () => {
- await _fixture.Client.GetInfoToAllAsync(
- "NonExisting",
- userCredentials: TestCredentials.Root
- );
- }
- );
+ async () => await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.Root));
[SupportsPSToAll.Fact]
public async Task throws_with_no_credentials() =>
- await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); });
+ await Assert.ThrowsAsync(async () =>
+ await _fixture.Client.GetInfoToAllAsync("NonExisting"));
[SupportsPSToAll.Fact]
public async Task throws_with_non_existing_user() =>
- await Assert.ThrowsAsync(
- async () => {
- await _fixture.Client.GetInfoToAllAsync(
- "NonExisting",
- userCredentials: TestCredentials.TestBadUser
- );
- }
- );
+ await Assert.ThrowsAsync(async () =>
+ await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.TestBadUser));
[SupportsPSToAll.Fact]
public async Task returns_result_with_normal_user_credentials() {
- var result = await _fixture.Client.GetInfoToAllAsync(
- GroupName,
- userCredentials: TestCredentials.TestUser1
- );
+ var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.TestUser1);
Assert.Equal("$all", result.EventSource);
}
- void AssertKeyAndValue(IDictionary items, string key) {
+ private void AssertKeyAndValue(IDictionary items, string key) {
Assert.True(items.ContainsKey(key));
Assert.True(items[key] > 0);
}
public class Fixture : EventStoreClientFixture {
+ private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription;
+ private IAsyncEnumerator? _enumerator;
public Fixture() : base(noDefaultCredentials: true) { }
protected override async Task Given() {
if (SupportsPSToAll.No)
return;
- await Client.CreateToAllAsync(
- GroupName,
- get_info.Settings,
- userCredentials: TestCredentials.Root
- );
+ await Client.CreateToAllAsync(GroupName, get_info.Settings, userCredentials: TestCredentials.Root);
+
+ foreach (var eventData in CreateTestEvents(20)) {
+ await StreamsClient.AppendToStreamAsync($"test-{Guid.NewGuid():n}", StreamState.NoStream,
+ new[] { eventData }, userCredentials: TestCredentials.Root);
+ }
}
protected override async Task When() {
@@ -157,26 +145,37 @@ protected override async Task When() {
return;
var counter = 0;
- var tcs = new TaskCompletionSource();
-
- await Client.SubscribeToAllAsync(
- GroupName,
- (s, e, r, ct) => {
- counter++;
-
- switch (counter) {
- case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e);
- break;
- case > 10:
- tcs.TrySetResult();
- break;
- }
- return Task.CompletedTask;
- },
- userCredentials: TestCredentials.Root
- );
-
- await tcs.Task;
+
+ _subscription = Client.SubscribeToAll(GroupName, userCredentials: TestCredentials.Root);
+ _enumerator = _subscription.Messages.GetAsyncEnumerator();
+
+ while (await _enumerator.MoveNextAsync()) {
+ if (_enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) {
+ continue;
+ }
+
+ counter++;
+
+ if (counter == 1) {
+ await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent);
+ }
+
+ if (counter > 10) {
+ return;
+ }
+ }
+ }
+
+ public override async Task DisposeAsync() {
+ if (_enumerator is not null) {
+ await _enumerator.DisposeAsync();
+ }
+
+ if (_subscription is not null) {
+ await _subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs
new file mode 100644
index 000000000..d000304bb
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info_obsolete.cs
@@ -0,0 +1,183 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class get_info_obsolete : IClassFixture {
+ const string GroupName = nameof(get_info_obsolete);
+
+ static readonly PersistentSubscriptionSettings Settings = new(
+ resolveLinkTos: true,
+ startFrom: Position.Start,
+ extraStatistics: true,
+ messageTimeout: TimeSpan.FromSeconds(9),
+ maxRetryCount: 11,
+ liveBufferSize: 303,
+ readBatchSize: 30,
+ historyBufferSize: 909,
+ checkPointAfter: TimeSpan.FromSeconds(1),
+ checkPointLowerBound: 1,
+ checkPointUpperBound: 1,
+ maxSubscriberCount: 500,
+ consumerStrategyName: SystemConsumerStrategies.Pinned
+ );
+
+ readonly Fixture _fixture;
+
+ public get_info_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [Fact]
+ public async Task throws_when_not_supported() {
+ if (SupportsPSToAll.No)
+ await Assert.ThrowsAsync(
+ async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); }
+ );
+ }
+
+ [SupportsPSToAll.Fact]
+ public async Task returns_expected_result() {
+ var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root);
+
+ Assert.Equal("$all", result.EventSource);
+ Assert.Equal(GroupName, result.GroupName);
+ Assert.Equal("Live", result.Status);
+
+ Assert.NotNull(Settings.StartFrom);
+ Assert.True(result.Stats.TotalItems > 0);
+ Assert.True(result.Stats.OutstandingMessagesCount > 0);
+ Assert.True(result.Stats.AveragePerSecond >= 0);
+ Assert.True(result.Stats.ParkedMessageCount > 0);
+ Assert.True(result.Stats.AveragePerSecond >= 0);
+ Assert.True(result.Stats.CountSinceLastMeasurement >= 0);
+ Assert.True(result.Stats.TotalInFlightMessages >= 0);
+ Assert.NotNull(result.Stats.LastKnownEventPosition);
+ Assert.NotNull(result.Stats.LastCheckpointedEventPosition);
+ Assert.True(result.Stats.LiveBufferCount >= 0);
+
+ Assert.NotNull(result.Connections);
+ Assert.NotEmpty(result.Connections);
+
+ var connection = result.Connections.First();
+ Assert.NotNull(connection.From);
+ Assert.Equal(TestCredentials.Root.Username, connection.Username);
+ Assert.NotEmpty(connection.ConnectionName);
+ Assert.True(connection.AverageItemsPerSecond >= 0);
+ Assert.True(connection.TotalItems > 0);
+ Assert.True(connection.CountSinceLastMeasurement >= 0);
+ Assert.True(connection.AvailableSlots >= 0);
+ Assert.True(connection.InFlightMessages >= 0);
+ Assert.NotNull(connection.ExtraStatistics);
+ Assert.NotEmpty(connection.ExtraStatistics);
+
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent);
+ AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent);
+
+ Assert.NotNull(result.Settings);
+ Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom);
+ Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos);
+ Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics);
+ Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout);
+ Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount);
+ Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize);
+ Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize);
+ Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize);
+ Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter);
+ Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound);
+ Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound);
+ Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount);
+ Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName);
+ }
+
+ [SupportsPSToAll.Fact]
+ public async Task throws_with_non_existing_subscription() =>
+ await Assert.ThrowsAsync(
+ async () => {
+ await _fixture.Client.GetInfoToAllAsync(
+ "NonExisting",
+ userCredentials: TestCredentials.Root
+ );
+ }
+ );
+
+ [SupportsPSToAll.Fact]
+ public async Task throws_with_no_credentials() =>
+ await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); });
+
+ [SupportsPSToAll.Fact]
+ public async Task throws_with_non_existing_user() =>
+ await Assert.ThrowsAsync(
+ async () => {
+ await _fixture.Client.GetInfoToAllAsync(
+ "NonExisting",
+ userCredentials: TestCredentials.TestBadUser
+ );
+ }
+ );
+
+ [SupportsPSToAll.Fact]
+ public async Task returns_result_with_normal_user_credentials() {
+ var result = await _fixture.Client.GetInfoToAllAsync(
+ GroupName,
+ userCredentials: TestCredentials.TestUser1
+ );
+
+ Assert.Equal("$all", result.EventSource);
+ }
+
+ void AssertKeyAndValue(IDictionary items, string key) {
+ Assert.True(items.ContainsKey(key));
+ Assert.True(items[key] > 0);
+ }
+
+ public class Fixture : EventStoreClientFixture {
+ public Fixture() : base(noDefaultCredentials: true) { }
+
+ protected override async Task Given() {
+ if (SupportsPSToAll.No)
+ return;
+
+ await Client.CreateToAllAsync(
+ GroupName,
+ get_info_obsolete.Settings,
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override async Task When() {
+ if (SupportsPSToAll.No)
+ return;
+
+ var counter = 0;
+ var tcs = new TaskCompletionSource();
+
+ await Client.SubscribeToAllAsync(
+ GroupName,
+ (s, e, r, ct) => {
+ counter++;
+
+ switch (counter) {
+ case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e);
+ break;
+ case > 10:
+ tcs.TrySetResult();
+ break;
+ }
+ return Task.CompletedTask;
+ },
+ userCredentials: TestCredentials.Root
+ );
+
+ await tcs.Task;
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs
index 1414e1599..dbcc44b43 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs
@@ -2,76 +2,56 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class happy_case_catching_up_to_link_to_events_manual_ack
+public class happy_case_catching_up_to_link_to_events_manual_ack
: IClassFixture {
- const string Group = nameof(Group);
- const int BufferCount = 10;
- const int EventWriteCount = BufferCount * 2;
+ private const string Group = nameof(Group);
+ private const int BufferCount = 10;
+ private const int EventWriteCount = BufferCount * 2;
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task Test() => await _fixture.EventsReceived.WithTimeout();
+ public async Task Test() {
+ await _fixture.Subscription!.Messages.OfType()
+ .Take(_fixture.Events.Length)
+ .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent))
+ .WithTimeout();
+
+ }
public class Fixture : EventStoreClientFixture {
- readonly EventData[] _events;
- readonly TaskCompletionSource _eventsReceived;
- int _eventReceivedCount;
+ public readonly EventData[] Events;
- PersistentSubscription? _subscription;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
public Fixture() {
- _events = CreateTestEvents(EventWriteCount)
- .Select(
- (e, i) => new EventData(
- e.EventId,
- SystemEventTypes.LinkTo,
- Encoding.UTF8.GetBytes($"{i}@test"),
- contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream
- )
- )
+ Events = CreateTestEvents(EventWriteCount)
+ .Select((e, i) => new EventData(e.EventId, SystemEventTypes.LinkTo, Encoding.UTF8.GetBytes($"{i}@test"),
+ contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream))
.ToArray();
-
- _eventsReceived = new();
}
- public Task EventsReceived => _eventsReceived.Task;
-
protected override async Task Given() {
- foreach (var e in _events)
+ foreach (var e in Events) {
await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e });
+ }
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: Position.Start, resolveLinkTos: true),
- userCredentials: TestCredentials.Root
- );
-
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, retryCount, ct) => {
- await subscription.Ack(e);
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true),
+ userCredentials: TestCredentials.Root);
- if (e.OriginalStreamId.StartsWith("test-")
- && Interlocked.Increment(ref _eventReceivedCount) == _events.Length)
- _eventsReceived.TrySetResult(true);
- },
- (s, r, e) => {
- if (e != null)
- _eventsReceived.TrySetException(e);
- },
- bufferSize: BufferCount,
- userCredentials: TestCredentials.Root
- );
+ Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root);
}
protected override Task When() => Task.CompletedTask;
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs
new file mode 100644
index 000000000..b56ea25a9
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs
@@ -0,0 +1,78 @@
+using System.Text;
+
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete
+ : IClassFixture {
+ const string Group = nameof(Group);
+ const int BufferCount = 10;
+ const int EventWriteCount = BufferCount * 2;
+
+ readonly Fixture _fixture;
+
+ public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task Test() => await _fixture.EventsReceived.WithTimeout();
+
+ public class Fixture : EventStoreClientFixture {
+ readonly EventData[] _events;
+ readonly TaskCompletionSource _eventsReceived;
+ int _eventReceivedCount;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() {
+ _events = CreateTestEvents(EventWriteCount)
+ .Select(
+ (e, i) => new EventData(
+ e.EventId,
+ SystemEventTypes.LinkTo,
+ Encoding.UTF8.GetBytes($"{i}@test"),
+ contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream
+ )
+ )
+ .ToArray();
+
+ _eventsReceived = new();
+ }
+
+ public Task EventsReceived => _eventsReceived.Task;
+
+ protected override async Task Given() {
+ foreach (var e in _events)
+ await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e });
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: Position.Start, resolveLinkTos: true),
+ userCredentials: TestCredentials.Root
+ );
+
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, retryCount, ct) => {
+ await subscription.Ack(e);
+
+ if (e.OriginalStreamId.StartsWith("test-")
+ && Interlocked.Increment(ref _eventReceivedCount) == _events.Length)
+ _eventsReceived.TrySetResult(true);
+ },
+ (s, r, e) => {
+ if (e != null)
+ _eventsReceived.TrySetException(e);
+ },
+ bufferSize: BufferCount,
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override Task When() => Task.CompletedTask;
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs
index 0eec37c6b..05612443d 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs
@@ -1,64 +1,51 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
-public class happy_case_catching_up_to_normal_events_manual_ack : IClassFixture {
- const string Group = nameof(Group);
- const int BufferCount = 10;
- const int EventWriteCount = BufferCount * 2;
+public class happy_case_catching_up_to_normal_events_manual_ack
+ : IClassFixture {
+ private const string Group = nameof(Group);
+ private const int BufferCount = 10;
+ private const int EventWriteCount = BufferCount * 2;
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture;
[SupportsPSToAll.Fact]
- public async Task Test() => await _fixture.EventsReceived.WithTimeout();
+ public async Task Test() {
+ await _fixture.Subscription!.Messages.OfType()
+ .Take(_fixture.Events.Length)
+ .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent))
+ .WithTimeout();
+ }
+
public class Fixture : EventStoreClientFixture {
- readonly EventData[] _events;
- readonly TaskCompletionSource _eventsReceived;
- int _eventReceivedCount;
+ public readonly EventData[] Events;
- PersistentSubscription? _subscription;
+ public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; }
public Fixture() {
- _events = CreateTestEvents(EventWriteCount).ToArray();
- _eventsReceived = new();
+ Events = CreateTestEvents(EventWriteCount).ToArray();
}
- public Task EventsReceived => _eventsReceived.Task;
-
protected override async Task Given() {
- foreach (var e in _events)
+ foreach (var e in Events)
await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e });
- await Client.CreateToAllAsync(
- Group,
- new(startFrom: Position.Start, resolveLinkTos: true),
- userCredentials: TestCredentials.Root
- );
+ await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true),
+ userCredentials: TestCredentials.Root);
- _subscription = await Client.SubscribeToAllAsync(
- Group,
- async (subscription, e, retryCount, ct) => {
- await subscription.Ack(e);
-
- if (e.OriginalStreamId.StartsWith("test-")
- && Interlocked.Increment(ref _eventReceivedCount) == _events.Length)
- _eventsReceived.TrySetResult(true);
- },
- (s, r, e) => {
- if (e != null)
- _eventsReceived.TrySetException(e);
- },
- bufferSize: BufferCount,
- userCredentials: TestCredentials.Root
- );
+ Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root);
}
protected override Task When() => Task.CompletedTask;
- public override Task DisposeAsync() {
- _subscription?.Dispose();
- return base.DisposeAsync();
+ public override async Task DisposeAsync() {
+ if (Subscription is not null) {
+ await Subscription.DisposeAsync();
+ }
+
+ await base.DisposeAsync();
}
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs
new file mode 100644
index 000000000..6431b9514
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs
@@ -0,0 +1,65 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture {
+ const string Group = nameof(Group);
+ const int BufferCount = 10;
+ const int EventWriteCount = BufferCount * 2;
+
+ readonly Fixture _fixture;
+
+ public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture;
+
+ [SupportsPSToAll.Fact]
+ public async Task Test() => await _fixture.EventsReceived.WithTimeout();
+
+ public class Fixture : EventStoreClientFixture {
+ readonly EventData[] _events;
+ readonly TaskCompletionSource _eventsReceived;
+ int _eventReceivedCount;
+
+ PersistentSubscription? _subscription;
+
+ public Fixture() {
+ _events = CreateTestEvents(EventWriteCount).ToArray();
+ _eventsReceived = new();
+ }
+
+ public Task EventsReceived => _eventsReceived.Task;
+
+ protected override async Task Given() {
+ foreach (var e in _events)
+ await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e });
+
+ await Client.CreateToAllAsync(
+ Group,
+ new(startFrom: Position.Start, resolveLinkTos: true),
+ userCredentials: TestCredentials.Root
+ );
+
+ _subscription = await Client.SubscribeToAllAsync(
+ Group,
+ async (subscription, e, retryCount, ct) => {
+ await subscription.Ack(e);
+
+ if (e.OriginalStreamId.StartsWith("test-")
+ && Interlocked.Increment(ref _eventReceivedCount) == _events.Length)
+ _eventsReceived.TrySetResult(true);
+ },
+ (s, r, e) => {
+ if (e != null)
+ _eventsReceived.TrySetException(e);
+ },
+ bufferSize: BufferCount,
+ userCredentials: TestCredentials.Root
+ );
+ }
+
+ protected override Task When() => Task.CompletedTask;
+
+ public override Task DisposeAsync() {
+ _subscription?.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs
index c7a5fb7c8..9d89fddb1 100644
--- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs
@@ -1,7 +1,7 @@
namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
public class happy_case_filtered : IClassFixture {
- readonly Fixture _fixture;
+ private readonly Fixture _fixture;
public happy_case_filtered(Fixture fixture) => _fixture = fixture;
@@ -14,58 +14,41 @@ public async Task reads_all_existing_filtered_events(string filterName) {
var (getFilter, prepareEvent) = Filters.GetFilter(filterName);
var filter = getFilter(streamPrefix);
- var appeared = new TaskCompletionSource();
var appearedEvents = new List();
- var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray();
+ var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray();
- foreach (var e in events)
- await _fixture.StreamsClient.AppendToStreamAsync(
- $"{streamPrefix}_{Guid.NewGuid():n}",
- StreamState.NoStream,
- new[] { e }
- );
-
- await _fixture.Client.CreateToAllAsync(
- filterName,
- filter,
- new(startFrom: Position.Start),
- userCredentials: TestCredentials.Root
- );
-
- using var subscription = await _fixture.Client.SubscribeToAllAsync(
- filterName,
- async (s, e, r, ct) => {
- appearedEvents.Add(e.Event);
- if (appearedEvents.Count >= events.Length)
- appeared.TrySetResult(true);
+ foreach (var e in events) {
+ await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream,
+ new[] { e });
+ }
- await s.Ack(e);
- },
- userCredentials: TestCredentials.Root
- )
+ await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: Position.Start),
+ userCredentials: TestCredentials.Root);
+
+ await using var subscription =
+ _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root);
+ await subscription.Messages
+ .OfType()
+ .Take(events.Length)
+ .ForEachAwaitAsync(async e => {
+ var (resolvedEvent, _) = e;
+ appearedEvents.Add(resolvedEvent.Event);
+ await subscription.Ack(resolvedEvent);
+ })
.WithTimeout();
- await Task.WhenAll(appeared.Task).WithTimeout();
-
Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId));
}
public class Fixture : EventStoreClientFixture {
protected override async Task Given() {
- await StreamsClient.AppendToStreamAsync(
- Guid.NewGuid().ToString(),
- StreamState.NoStream,
- CreateTestEvents(256)
- );
+ await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream,
+ CreateTestEvents(256));
- await StreamsClient.SetStreamMetadataAsync(
- SystemStreams.AllStream,
- StreamState.Any,
- new(acl: new(SystemRoles.All)),
- userCredentials: TestCredentials.Root
- );
+ await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any,
+ new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root);
}
protected override Task When() => Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs
new file mode 100644
index 000000000..aba17050d
--- /dev/null
+++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_obsolete.cs
@@ -0,0 +1,72 @@
+namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll;
+
+[Obsolete]
+public class happy_case_filtered_obsolete : IClassFixture {
+ readonly Fixture _fixture;
+
+ public happy_case_filtered_obsolete(Fixture fixture) => _fixture = fixture;
+
+ public static IEnumerable