Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ trim_trailing_whitespace = false

[*.ps1]
end_of_line = lf

[*.{yml,yaml}]
indent_size = 2
6 changes: 2 additions & 4 deletions .github/workflows/build-on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ name: Build ON push

on:
push:
branches: [ master, main ]
branches: [master, main]

jobs:

build:
runs-on: ubuntu-latest
steps:
- name: Run ESDB image
run: docker run -d --name esdbnode -it -p 2113:2113 -p 1113:1113 eventstore/eventstore:latest --insecure --run-projections=All
run: docker run -d --name esdbnode -it -p 2113:2113 -p 1113:1113 eventstore/eventstore:latest --insecure --run-projections=All --enable-atom-pub-over-http
- name: Checkout
uses: actions/checkout@v2
- name: Setup .NET
Expand All @@ -28,4 +27,3 @@ jobs:
- name: Test
run: dotnet test --no-build --verbosity minimal
working-directory: ./src

Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,4 @@
<ProjectReference Include="..\BullOak.Repositories.EventStore\BullOak.Repositories.EventStore.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="EventStoreServer\EventStore.ClusterNode.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using EventStore.Client;
using EventStore.Client.Projections;

namespace BullOak.Repositories.EventStore.Test.Integration.Contexts
{
Expand All @@ -14,6 +13,8 @@ namespace BullOak.Repositories.EventStore.Test.Integration.Contexts
using System.Threading.Tasks;
using Events;
using TechTalk.SpecFlow;
using Polly;
using FluentAssertions;

internal class EventStoreIntegrationContext
{
Expand Down Expand Up @@ -85,33 +86,129 @@ public async Task AppendEventsToCurrentStream(string id, IMyEvent[] events)
}
}

public async Task TruncateStream(string id)
{
var connection = GetConnection();

var lastEventResult = connection.ReadStreamAsync(Direction.Backwards, id, StreamPosition.End, 1);
ResolvedEvent lastEvent = default;
await foreach (var e in lastEventResult)
{
lastEvent = e;
break;
}

if (lastEvent.OriginalEventNumber >= 0)
{
var metadata = await connection.GetStreamMetadataAsync(id);

await connection.SetStreamMetadataAsync(
id,
metadata.MetastreamRevision.HasValue
? new StreamRevision(metadata.MetastreamRevision.Value)
: StreamRevision.None,
new StreamMetadata(truncateBefore: lastEvent.OriginalEventNumber + 1));
}
}

public async Task AssertStreamHasNoResolvedEvents(string id)
{
var retry = Policy
.HandleResult(true)
.WaitAndRetryAsync(3, count => TimeSpan.FromMilliseconds(500));

var anyResolvedEvents = await retry.ExecuteAsync(async () =>
{
var connection = GetConnection();
var result = connection.ReadStreamAsync(
Direction.Forwards,
id,
revision: StreamPosition.Start,
resolveLinkTos: true);
var eventFound = false;

await foreach (var x in result)
{
if (x.IsResolved)
{
eventFound = true;
break;
}
}
return eventFound;
});


anyResolvedEvents.Should().BeFalse();
}

public async Task AssertStreamHasSomeUnresolvedEvents(string id)
{
var retry = Policy
.HandleResult(false)
.WaitAndRetryAsync(3, count => TimeSpan.FromMilliseconds(500));

var anyUnresolvedEvents = await retry.ExecuteAsync(async () =>
{
try
{
var connection = GetConnection();
var result = connection.ReadStreamAsync(
Direction.Forwards,
id,
revision: StreamPosition.Start,
resolveLinkTos: true);
var eventFound = false;

await foreach (var x in result)
{
if (!x.IsResolved)
{
eventFound = true;
break;
}
}

return eventFound;
}
catch (StreamNotFoundException)
{
return false;
}
});


anyUnresolvedEvents.Should().BeTrue();
}

public Task SoftDeleteStream(string id)
=> repository.SoftDelete(id);
=> GetConnection().SoftDeleteAsync(id, StreamState.Any);

public Task HardDeleteStream(string id)
=> GetConnection().TombstoneAsync(id, StreamState.Any);

public Task SoftDeleteByEvent(string id)
public Task SoftDeleteStreamFromRepository(string id)
=> repository.SoftDelete(id);
public Task SoftDeleteFromRepositoryBySoftDeleteEvent(string id)
=> repository.SoftDeleteByEvent(id);

public Task SoftDeleteByEvent<TSoftDeleteEvent>(string id, Func<TSoftDeleteEvent> createSoftDeleteEvent)
public Task SoftDeleteFromRepositoryBySoftDeleteEvent<TSoftDeleteEvent>(string id, Func<TSoftDeleteEvent> createSoftDeleteEvent)
where TSoftDeleteEvent : EntitySoftDeleted
=> repository.SoftDeleteByEvent(id, createSoftDeleteEvent);

public async Task<ResolvedEvent[]> ReadEventsFromStreamRaw(string id)
{
var client = GetConnection();
var result = new List<ResolvedEvent>();
var readResults = client.ReadStreamAsync(Direction.Forwards, id, StreamPosition.Start);
var connection = GetConnection();
var readResults = connection.ReadStreamAsync(Direction.Forwards, id, StreamPosition.Start);

return await readResults.ToArrayAsync();
}

internal async Task WriteEventsToStreamRaw(string currentStreamInUse, IEnumerable<MyEvent> myEvents)
{
var conn = await SetupConnection();
var connection = GetConnection();

await conn.AppendToStreamAsync(currentStreamInUse, StreamState.Any,
await connection.AppendToStreamAsync(currentStreamInUse, StreamState.Any,
myEvents.Select(e =>
{
var serialized = JsonConvert.SerializeObject(e);
Expand All @@ -130,6 +227,7 @@ private static async Task<EventStoreClient> SetupConnection()
var client = new EventStoreClient(settings);
var projectionsClient = new EventStoreProjectionManagementClient(settings);
await projectionsClient.EnableAsync("$by_category");
await projectionsClient.EnableAsync("$by_event_type");

await Task.Delay(TimeSpan.FromSeconds(3));

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Feature: EventStore Metadata Support
In order to be able to use the library with any EventStore stream
As a developer using this library
I want to be able to use session with regular streams and projections containing
metadata entries not related to regular events, e.g. links to missing events

Scenario: Open EventStore deleted stream

As opposed to event-based soft deleted stream (by using custom EntitySoftDeleted event),
EventStore soft-deleted stream is a stream that has been deleted via ES Admin Console
or by using HTTP DELETE operation. ES adds a soft-delete metadata marker and scavenges (eventually)
all the existing events from the stream.

Given a stream with events
And I delete the stream in EventStore
When I try to open the stream
Then the session reports new state

Scenario: Append events to EventStore deleted stream

Given a stream with events
And I delete the stream in EventStore
And I add some new events to the stream
When I try to open the stream
Then the session can rehydrate the state

Scenario: Open EventStore truncated stream

EventStore allows to truncate all the events in the stream before a given event version N.
ES modifies the stream metadata and scavenges (eventually) all the events before version N.
Resulting stream looks just like a non-truncated stream, except the first event in the stream
has id N and not 0.

Semantically, an empty stream after being truncated is the same as an empty projection

Given a stream with events
And I truncate the stream in EventStore
When I try to open the stream
Then the session reports new state

Scenario: Append events to EventStore truncated stream

Given a stream with events
And I truncate the stream in EventStore
And I add some new events to the stream
When I try to open the stream
Then the session can rehydrate the state

Scenario: Open EventStore projection pointing to undefined events

Session can be used pointing to a projection that does not include any events.

When I try to open a projection that uses undefined events
Then the session reports new state

Scenario: Open EventStore projection containing links to deleted events

https://developers.eventstore.com/server/v20.10/docs/streams/deleting-streams-and-events.html#deleted-events-and-projections

Projections may contain metadata entries that do no resolve to an actual event
because the original event was truncated.

Given a stream with events
And I truncate the stream in EventStore
And I add some new events to the stream
When I try to open a projection that uses events from the truncated stream
Then the session can rehydrate the state
Loading