This is a beginner question, because I don't have any experience in using NEventStore and I'm giving it a try.
The point of this question is related to the concept of optimistic concurrency check as envisioned by Greg Young in this document for which a practical example is given here.
So, in my application I have the following interface which is the event store abstraction that I'm going to use in the repository implementation:
public interface IEventStore
{
void SaveEvents(Guid aggregateId, IEnumerable<Event> events, int expectedVersion);
List<Event> GetEventsForAggregate(Guid aggregateId);
}
My goal is providing an implementation of IEventStore
by using the NEventStore library. This is a naive implementation, where I perform a kind of poor man optimistic concurrency check:
public class EventStore : IEventStore
{
private readonly IEventStream stream;
public EventStore(IEventStream stream)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
public List<Event> GetEventsForAggregate(Guid aggregateId)
{
// implementation omitted because I'm only interested in understanding how to imeplement save method right now...
}
public void SaveEvents(Guid aggregateId, IEnumerable<Event> events, int expectedVersion)
{
using (var stream = this.store.OpenStream(aggregateId, 0, int.MaxValue))
{
// here, by following Greg Young's paper, I should perform the optimistic concurrency check...
int currentAggregateRevision = stream.StreamRevision;
bool isNewStream = currentAggregateRevision == 0;
if(!isNewStream && expectedVersion != currentAggregateRevision)
{
// DANGER: optimistic concurrency check failed !
throw new ConcurrencyException("The guy that issued the command did not work on the latest version of the aggregate. We cannot commit these events.")
}
foreach(var @event in events)
{
stream.Add(new EventMessage { Body = @event });
}
stream.CommitChanges(Guid.NewGuid()); // Is there a best practice to generate a commit id ? Is it ok to use a new guid ?
}
}
}
Here are my questions:
is there a way to avoid the manual check on the expected aggregate version before saving the new commit, by using some capability of NEventStore ?
I'm assuming here that NEventStore is able to lock the stream while the expected aggregate version is checked, so that if another thread or node write to the same stream between the call to
OpenStream
andCommitChanges
aConcurrencyException
is raised whenCommitChanges
is called. Is this assumption correct ?
UPDATE 3 JUNE 2019
For all the readers interested in the subject, I asked the same question on the NEventStore github repo. Look at the issue I opened if you are interested in the discussion.
UPDATE 7 JUNE 2019
Here is the final code version I came up with by following the suggestions I got from the github issue:
public class EventStore : IEventStore
{
private readonly IEventStream stream;
public EventStore(IEventStream stream)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
public List<Event> GetEventsForAggregate(Guid aggregateId)
{
// implementation omitted because I'm only interested in understanding how to imeplement save method right now...
}
public void SaveEvents(Guid aggregateId, IEnumerable<Event> events, int expectedVersion)
{
if(expectedVersion < 0)
{
throws new ArgumentOutOfRangeException(nameof(expectedVersion));
}
bool isNewStream = expectedVersion == 0;
if(isNewStream) {
using (var stream = this.store.CreateStream(aggregateId))
{
foreach(var @event in events)
{
stream.Add(new EventMessage { Body = @event });
}
stream.CommitChanges(Guid.NewGuid()); // throws ConcurrencyException if an aggregate with the same id already exists...
}
} else {
using (var stream = this.store.OpenStream(aggregateId, 0, expectedVersion))
{
foreach(var @event in events)
{
stream.Add(new EventMessage { Body = @event });
}
stream.CommitChanges(Guid.NewGuid()); // throws ConcurrencyException in case of concurrency issues...
}
}
}
}