public async Task<StreamEventsPage> ReadStream(string streamId, int fromVersion, int count, StreamReadDirection direction = StreamReadDirection.Forward)
        {
            Guard.NullOrWhiteSpace(() => streamId);

            await _log.Debug("Reading stream {@streamId} from version {fromVersion} to version {count}", streamId, fromVersion, count);

            // create parameters
            var parameters = new DynamicParameters();

            parameters.AddDynamicParams(new {
                StreamId    = streamId,
                FromVersion = fromVersion,
                Count       = count,
                ReadForward = direction == StreamReadDirection.Forward ? 1 : 0
            });

            parameters.AddOutput("Error");
            parameters.AddReturnValue();

            IEnumerable<StreamEventData> result;

            // execute operation
            using(var connection = new SqlConnection(_settings.ConnectionString)) {
                // found a hardcore bug with Dapper!
                // if we exit a query without executing a select, the .QueryAsync<T> fails because it
                // tries to read the resultsetschema and fails. 
                // therefore we do not have access to return or output values.
                result = await connection
                    .QueryAsync<StreamEventData>(
                        sql        : "ReadStream",
                        param      : parameters,
                        commandType: CommandType.StoredProcedure)
                    .ConfigureAwait(false);
            }

            // check for errors 
            switch(parameters.GetOutput<int>("Error")) {
                case -100:
                    throw new StreamNotFoundException(streamId);
                case -200:
                    throw new StreamDeletedException(streamId, fromVersion);
            }

            await _log.Information("Stream {@streamId} read from version {fromVersion} to version {count}", streamId, fromVersion, count);

            // return stream page
            var streamVersion = parameters.GetReturnValue();

            var streamEvents = result.Select(
                streamEventData => {
                    var metadata        = _serializer.Deserialize<IDictionary<string, string>>(streamEventData.Metadata);
                    var domainEventType = Type.GetType(metadata[EventMetadataKeys.ClrType].ToString());
                    var streamEvent     = _serializer.DeserializeAs<object>(streamEventData.Data, domainEventType);
                    return new StreamEvent(streamEvent, metadata);
                }).ToList();

            var lastReadEventVersion = streamEvents.Any()
                ? int.Parse(streamEvents.Last().Metadata[EventMetadataKeys.Version])
                : -1;

            await _log.Debug("Stream {@streamId} event serialization finished", streamId);

            return new StreamEventsPage(
                streamId   : streamId,
                fromVersion: fromVersion,
                toVersion  : lastReadEventVersion,
                lastVersion: streamVersion,           
                events     : streamEvents,
                direction  : direction);
        }