Some examples of edge cases that have to be watched out for in GetEventStore, starting with the read API. Here's the F#-API type for ReadStreamEventsForwardAsync:
/// <see cref="EventStore.ClientAPI.StreamEventsSlice" />
type StreamEventsSlice =
| NotFound of StreamId
| Deleted of StreamId
| Success of StreamSlice
/// Convert a <see cref="EventStore.ClientAPI.StreamEventsSlice" /> to a
/// ConnectionApi.EventsSlice.
and StreamSlice =
| StreamSlice of Slice
| EndOfStream of Slice
and Slice =
{ Events : ResolvedEvent list
; Stream : StreamId
; FromEventNumber : uint32
; ReadDirection : ReadDirection
; NextEventNumber : uint32
; LastEventNumber : uint32 }
and StreamId = string
and StreamId = stringHere's the corresponding decompiled API source:
public class StreamEventsSlice
{
public readonly SliceReadStatus Status;
public readonly string Stream;
public readonly int FromEventNumber;
public readonly ReadDirection ReadDirection;
public readonly EventStore.ClientAPI.ResolvedEvent[] Events;
public readonly int NextEventNumber;
public readonly int LastEventNumber;
public readonly bool IsEndOfStream;
internal StreamEventsSlice(SliceReadStatus status, string stream, int fromEventNumber, ReadDirection readDirection, ClientMessage.ResolvedIndexedEvent[] events, int nextEventNumber, int lastEventNumber, bool isEndOfStream)
{
Ensure.NotNullOrEmpty(stream, "stream");
this.Status = status;
this.Stream = stream;
this.FromEventNumber = fromEventNumber;
this.ReadDirection = readDirection;
if (events == null || events.Length == 0)
{
this.Events = Empty.ResolvedEvents;
}
else
{
this.Events = new EventStore.ClientAPI.ResolvedEvent[events.Length];
for (int index = 0; index < this.Events.Length; ++index)
this.Events[index] = new EventStore.ClientAPI.ResolvedEvent(events[index]);
}
this.NextEventNumber = nextEventNumber;
this.LastEventNumber = lastEventNumber;
this.IsEndOfStream = isEndOfStream;
}
}Leaving aside the pain of having public APIs with internal c'tors (forcing this darling* to come to life), it is clear there are some implicit invariants:
- We might get IsEndOfStream = true and then all other properties are pretty much possible to ignore. This has to be abducted.
- The stream can both be not found (Status = StreamReadStatus.NotFound) and,
- previously deleted (Status = StreamReadStatus.Deleted) or it can be
- Found, having data.
Besides that, the call to ReadStreamEventsForwardAsync has two System.Int32 parameters, which always have to be zero or a positive number. Why not use System.UInt32? Also, passing a zero count makes no sense, so that should really be always a positive number.
It becomes even more interesting when passing expectedVersion and to tell the API -2 (int) for no optimistic concurrency checking, -1 for NoStream, 0 for empty stream, having runtime exceptions in all other negative cases, and on top of that, having a possibility of getting WrongExpectedVersionException as an exception on the asynchronous call, if the optimistic check doesn't pass.
footnote:
// returns the constructor for the instance and a setter function, as a tuple, respectively
let reflPkgFor<'a> () : (unit -> 'a) * ('a -> string -> obj -> unit) =
let setterFor name = typeof<'a>.GetField(name, System.Reflection.BindingFlags.Instance ||| System.Reflection.BindingFlags.NonPublic)
let memSetterFor = memoize setterFor
let emptyCtor = fun () -> System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof<'a>) :?> 'a
let setter (instance : 'a) prop (value : obj) = memSetterFor prop |> (fun fi -> fi.SetValue(instance, value))
emptyCtor, setter
@anakryko
Thanks for your input. I noticed the same thing about EndOfStream (updated gist to my current code) - but it goes to show that the returned data doesn't show me how to use it in C#.
The comments that you added about LastEventNumber are interesting and definitely something hadn't heard of before. Thanks for telling me about it.