public void ProjectAsyncTokenMessagesCancellationAfterSecondHandlerCausesExpectedResult()
        {
            var tcs = new CancellationTokenSource();

            Func <object, object, CancellationToken, Task> handler1 =
                (connection, message, token) => Task.FromResult <object>(null);
            Func <object, object, CancellationToken, Task> handler2 =
                (connection, message, token) =>
            {
                tcs.Cancel();
                return(Task.FromResult <object>(null));
            };
            Func <object, object, CancellationToken, Task> handler3 =
                (connection, message, token) =>
            {
                throw new InvalidOperationException("Should not happen!");
            };

            ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[]
            {
                new ConnectedProjectionHandler <object>(typeof(object), handler1),
                new ConnectedProjectionHandler <object>(typeof(object), handler2),
                new ConnectedProjectionHandler <object>(typeof(object), handler3)
            });
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object(), new object(), new object() }, tcs.Token),
                        Throws.Nothing);
        }
Beispiel #2
0
 public void SetUp()
 {
     _resolver = Resolve.
                 WhenEqualToHandlerMessageType(new ConnectedProjectionHandler <object> [0]);
     _messages = new[] { new object(), new object() };
     _sut      = new ConnectedProjectionScenario <object>(_resolver).Given(_messages);
 }
        public static IEnumerable <TestCaseData> ProjectMessageWithoutTokenCases()
        {
            var task = TaskFactory();
            //Match
            var message1 = new object();
            var handler1 = new ConnectedProjectionHandler <CallRecordingConnection>(
                typeof(object),
                (connection, message, token) =>
            {
                connection.RecordCall(1, message, token);
                return(task);
            });
            var resolver1 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new[] { handler1 });

            yield return(new TestCaseData(
                             resolver1,
                             message1,
                             new[]
            {
                new Tuple <int, object, CancellationToken>(1, message1, CancellationToken.None),
            }));

            //Mismatch
            var message2  = new object();
            var resolver2 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new ConnectedProjectionHandler <CallRecordingConnection> [0]);

            yield return(new TestCaseData(
                             resolver2,
                             message2,
                             new Tuple <int, object, CancellationToken> [0]));

            //Multimatch
            var message3 = new object();
            var handler3 = new ConnectedProjectionHandler <CallRecordingConnection>(
                typeof(object),
                (connection, message, token) =>
            {
                connection.RecordCall(3, message, token);
                return(task);
            });
            var handler4 = new ConnectedProjectionHandler <CallRecordingConnection>(
                typeof(object),
                (connection, message, token) =>
            {
                connection.RecordCall(4, message, token);
                return(task);
            });
            var resolver3 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new[] { handler3, handler4 });

            yield return(new TestCaseData(
                             resolver3,
                             message3,
                             new[]
            {
                new Tuple <int, object, CancellationToken>(3, message3, CancellationToken.None),
                new Tuple <int, object, CancellationToken>(4, message3, CancellationToken.None)
            }));
        }
 /// <summary>
 /// Initializes a new instance of the <see cref="ConnectedProjectionScenario{TConnection}"/> class.
 /// </summary>
 /// <param name="resolver">The projection handler resolver.</param>
 /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception>
 public ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver)
 {
     if (resolver == null)
     {
         throw new ArgumentNullException("resolver");
     }
     _resolver = resolver;
     _messages = new object[0];
 }
Beispiel #5
0
        /// <summary>
        /// Initializes a new instance of the <see cref="ConnectedProjector{TConnection}"/> class.
        /// </summary>
        /// <param name="resolver">The handler resolver.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception>
        public ConnectedProjector(ConnectedProjectionHandlerResolver <TConnection> resolver)
        {
            if (resolver == null)
            {
                throw new ArgumentNullException("resolver");
            }

            _resolver = resolver;
        }
Beispiel #6
0
        public void ProjectAsyncTokenMessagesResolverFailureCausesExpectedResult()
        {
            ConnectedProjectionHandlerResolver <object> resolver = m => throw new Exception("message");
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object(), new object() }, new CancellationToken()),
                        Throws.TypeOf <Exception>().And.Message.EqualTo("message"));
        }
        public async Task ProjectAsyncMessageCausesExpectedCalls(
            ConnectedProjectionHandlerResolver <CallRecordingConnection> resolver,
            object message,
            Tuple <int, object, CancellationToken>[] expectedCalls)
        {
            var connection = new CallRecordingConnection();
            var sut        = SutFactory(resolver);

            await sut.ProjectAsync(connection, message);

            Assert.That(connection.RecordedCalls, Is.EquivalentTo(expectedCalls));
        }
        public void ProjectAsyncTokenMessagesFirstHandlerFailureCausesExpectedResult()
        {
            Func <object, object, CancellationToken, Task> handler =
                (connection, message, token) =>
            {
                var source = new TaskCompletionSource <object>();
                source.SetException(new Exception("message"));
                return(source.Task);
            };
            ConnectedProjectionHandlerResolver <object> resolver = m => new[] { new ConnectedProjectionHandler <object>(typeof(object), handler) };
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object(), new object() }, new CancellationToken()),
                        Throws.TypeOf <Exception>().And.Message.EqualTo("message"));
        }
 /// <summary>
 /// Initializes a new instance of the <see cref="ConnectedProjectionTestSpecification{TConnection}"/> class.
 /// </summary>
 /// <param name="resolver">The projection handler resolver.</param>
 /// <param name="messages">The messages to project.</param>
 /// <param name="verification">The verification method.</param>
 /// <exception cref="System.ArgumentNullException">Thrown when
 /// <paramref name="resolver"/>
 /// or
 /// <paramref name="messages"/>
 /// or
 /// <paramref name="verification"/> is null.
 /// </exception>
 public ConnectedProjectionTestSpecification(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages, Func <TConnection, CancellationToken, Task <VerificationResult> > verification)
 {
     if (resolver == null)
     {
         throw new ArgumentNullException("resolver");
     }
     if (messages == null)
     {
         throw new ArgumentNullException("messages");
     }
     if (verification == null)
     {
         throw new ArgumentNullException("verification");
     }
     _resolver     = resolver;
     _messages     = messages;
     _verification = verification;
 }
        public void ProjectAsyncTokenMessagesSecondHandlerCancellationCausesExpectedResult()
        {
            Func <object, object, CancellationToken, Task> handler1 =
                (connection, message, token) => Task.FromResult <object>(null);
            Func <object, object, CancellationToken, Task> handler2 =
                (connection, message, token) =>
            {
                var source = new TaskCompletionSource <object>();
                source.SetCanceled();
                return(source.Task);
            };
            ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[]
            {
                new ConnectedProjectionHandler <object>(typeof(object), handler1),
                new ConnectedProjectionHandler <object>(typeof(int), handler2)
            });
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object(), new int() }, new CancellationToken()),
                        Throws.TypeOf <TaskCanceledException>());
        }
Beispiel #11
0
        public void ProjectAsyncMessageSecondHandlerFailureCausesExpectedResult()
        {
            Func <object, object, CancellationToken, Task> handler1 =
                (connection, message, token) => Task.FromResult <object>(null);
            Func <object, object, CancellationToken, Task> handler2 =
                (connection, message, token) =>
            {
                var source = new TaskCompletionSource <object>();
                source.SetException(new Exception("message"));
                return(source.Task);
            };
            ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[]
            {
                new ConnectedProjectionHandler <object>(typeof(object), handler1),
                new ConnectedProjectionHandler <object>(typeof(int), handler2)
            });
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new int()),
                        Throws.TypeOf <AggregateException>().And.InnerException.TypeOf <Exception>().And.InnerException.Message.EqualTo("message"));
        }
        public void ProjectAsyncTokenMessagesCancellationAfterSecondHandlerInContinuationCausesExpectedResult()
        {
            var tcs = new CancellationTokenSource();

            tcs.Cancel();

            Func <object, object, CancellationToken, Task> handler1 =
                async(connection, message, token) =>
            {
                await Task.Yield();

                await Task.Delay(TimeSpan.FromDays(1), token);
            };

            ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[]
            {
                new ConnectedProjectionHandler <object>(typeof(object), handler1),
            });
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object() }, tcs.Token),
                        Throws.TypeOf <TaskCanceledException>());
        }
        public void ProjectAsyncTokenMessagesHandlerObservingCancelledTokenCausesExpectedResult()
        {
            var tcs = new CancellationTokenSource();

            Func <object, object, CancellationToken, Task> handler1 =
                (connection, message, token) =>
            {
                var source = new TaskCompletionSource <object>();
                token.Register(() => source.SetCanceled());

                tcs.Cancel();
                return(source.Task);
            };

            ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[]
            {
                new ConnectedProjectionHandler <object>(typeof(object), handler1),
            });
            var sut = SutFactory(resolver);

            Assert.That(async() =>
                        await sut.ProjectAsync(new object(), new[] { new object() }, tcs.Token),
                        Throws.TypeOf <TaskCanceledException>());
        }
Beispiel #14
0
 /// <summary>
 /// Initializes a new instance of the <see cref="ConnectedProjector{TConnection}"/> class.
 /// </summary>
 /// <param name="resolver">The handler resolver.</param>
 /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception>
 public ConnectedProjector(ConnectedProjectionHandlerResolver <TConnection> resolver)
 => _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
 /// <summary>
 /// Initializes a new instance of the <see cref="ConnectedProjectionTestSpecification{TConnection}"/> class.
 /// </summary>
 /// <param name="resolver">The projection handler resolver.</param>
 /// <param name="messages">The messages to project.</param>
 /// <param name="verification">The verification method.</param>
 /// <exception cref="System.ArgumentNullException">Thrown when
 /// <paramref name="resolver"/>
 /// or
 /// <paramref name="messages"/>
 /// or
 /// <paramref name="verification"/> is null.
 /// </exception>
 public ConnectedProjectionTestSpecification(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages, Func <TConnection, CancellationToken, Task <VerificationResult> > verification)
 {
     Resolver     = resolver ?? throw new ArgumentNullException(nameof(resolver));
     Messages     = messages ?? throw new ArgumentNullException(nameof(messages));
     Verification = verification ?? throw new ArgumentNullException(nameof(verification));
 }
 private static ConnectedProjector <TConnection> SutFactory <TConnection>(ConnectedProjectionHandlerResolver <TConnection> resolver)
 {
     return(new ConnectedProjector <TConnection>(resolver));
 }
Beispiel #17
0
 /// <summary>
 /// Initializes a new instance of the <see cref="ConnectedProjectionScenario{TConnection}"/> class.
 /// </summary>
 /// <param name="resolver">The projection handler resolver.</param>
 /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception>
 public ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver)
 {
     _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
     _messages = new object[0];
 }
Beispiel #18
0
 public static ConnectedProjectionScenario <TConnection> Create <TConnection>(ConnectedProjectionHandlerResolver <TConnection> resolver)
 {
     return(new ConnectedProjectionScenario <TConnection>(resolver));
 }
        public EventProcessor(
            IStreamStore streamStore,
            AcceptStreamMessageFilter filter,
            EnvelopeFactory envelopeFactory,
            ConnectedProjectionHandlerResolver <EditorContext> resolver,
            Func <EditorContext> dbContextFactory,
            Scheduler scheduler,
            ILogger <EventProcessor> logger)
        {
            if (streamStore == null)
            {
                throw new ArgumentNullException(nameof(streamStore));
            }
            if (filter == null)
            {
                throw new ArgumentNullException(nameof(filter));
            }
            if (envelopeFactory == null)
            {
                throw new ArgumentNullException(nameof(envelopeFactory));
            }
            if (resolver == null)
            {
                throw new ArgumentNullException(nameof(resolver));
            }
            if (dbContextFactory == null)
            {
                throw new ArgumentNullException(nameof(dbContextFactory));
            }

            _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
            _logger    = logger ?? throw new ArgumentNullException(nameof(logger));

            _messagePumpCancellation = new CancellationTokenSource();
            _messageChannel          = Channel.CreateUnbounded <object>(new UnboundedChannelOptions
            {
                SingleReader = true,
                SingleWriter = false,
                AllowSynchronousContinuations = false
            });
            _messagePump = Task.Factory.StartNew(async() =>
            {
                IAllStreamSubscription subscription = null;
                try
                {
                    logger.LogInformation("EventProcessor message pump entered ...");
                    while (await _messageChannel.Reader.WaitToReadAsync(_messagePumpCancellation.Token).ConfigureAwait(false))
                    {
                        while (_messageChannel.Reader.TryRead(out var message))
                        {
                            switch (message)
                            {
                            case Resume _:
                                logger.LogInformation("Resuming ...");
                                await using (var resumeContext = dbContextFactory())
                                {
                                    var projection =
                                        await resumeContext.ProjectionStates
                                        .SingleOrDefaultAsync(
                                            item => item.Name == RoadRegistryEditorProjectionHost,
                                            _messagePumpCancellation.Token)
                                        .ConfigureAwait(false);
                                    var after = projection?.Position;
                                    var head  = await streamStore.ReadHeadPosition();
                                    if (head == Position.Start || (after.HasValue
                                            ? head - after.Value <= CatchUpThreshold
                                            : head - CatchUpThreshold <= 0))
                                    {
                                        await _messageChannel.Writer
                                        .WriteAsync(new Subscribe(after), _messagePumpCancellation.Token)
                                        .ConfigureAwait(false);
                                    }
                                    else
                                    {
                                        await _messageChannel.Writer
                                        .WriteAsync(new CatchUp(after, CatchUpBatchSize), _messagePumpCancellation.Token)
                                        .ConfigureAwait(false);
                                    }
                                }

                                break;

                            case CatchUp catchUp:
                                logger.LogInformation("Catching up as of {Position}", catchUp.AfterPosition ?? -1L);
                                var observedMessageCount = 0;
                                var catchUpPosition      = catchUp.AfterPosition ?? Position.Start;
                                var context = dbContextFactory();
                                var page    = await streamStore
                                              .ReadAllForwards(
                                    catchUpPosition,
                                    catchUp.BatchSize,
                                    true,
                                    _messagePumpCancellation.Token)
                                              .ConfigureAwait(false);

                                while (!page.IsEnd)
                                {
                                    foreach (var streamMessage in page.Messages)
                                    {
                                        if (catchUp.AfterPosition.HasValue &&
                                            streamMessage.Position == catchUp.AfterPosition.Value)
                                        {
                                            continue;     // skip already processed message
                                        }

                                        if (filter(streamMessage))
                                        {
                                            logger.LogInformation("Catching up on {MessageType} at {Position}",
                                                                  streamMessage.Type, streamMessage.Position);
                                            var envelope = envelopeFactory.Create(streamMessage);
                                            var handlers = resolver(envelope);
                                            foreach (var handler in handlers)
                                            {
                                                await handler
                                                .Handler(context, envelope, _messagePumpCancellation.Token)
                                                .ConfigureAwait(false);
                                            }
                                        }

                                        observedMessageCount++;
                                        catchUpPosition = streamMessage.Position;

                                        if (observedMessageCount % CatchUpBatchSize == 0)
                                        {
                                            logger.LogInformation(
                                                "Flushing catch up position of {0} and persisting changes ...",
                                                catchUpPosition);
                                            await context
                                            .UpdateProjectionState(
                                                RoadRegistryEditorProjectionHost,
                                                catchUpPosition,
                                                _messagePumpCancellation.Token)
                                            .ConfigureAwait(false);
                                            await context.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false);
                                            await context.DisposeAsync().ConfigureAwait(false);

                                            context = dbContextFactory();
                                            observedMessageCount = 0;
                                        }
                                    }

                                    page = await page.ReadNext(_messagePumpCancellation.Token).ConfigureAwait(false);
                                }

                                if (observedMessageCount > 0)     // case where we just read the last page and pending work in memory needs to be flushed
                                {
                                    logger.LogInformation(
                                        "Flushing catch up position of {Position} and persisting changes ...",
                                        catchUpPosition);
                                    await context
                                    .UpdateProjectionState(
                                        RoadRegistryEditorProjectionHost,
                                        catchUpPosition,
                                        _messagePumpCancellation.Token)
                                    .ConfigureAwait(false);
                                    await context.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false);
                                }

                                await context.DisposeAsync().ConfigureAwait(false);

                                //switch to subscription as of the last page
                                await _messageChannel.Writer
                                .WriteAsync(
                                    new Subscribe(catchUpPosition),
                                    _messagePumpCancellation.Token)
                                .ConfigureAwait(false);
                                break;

                            case Subscribe subscribe:
                                logger.LogInformation("Subscribing as of {0}", subscribe.AfterPosition ?? -1L);
                                subscription?.Dispose();
                                subscription = streamStore.SubscribeToAll(
                                    subscribe.AfterPosition, async(_, streamMessage, token) =>
                                {
                                    if (filter(streamMessage))
                                    {
                                        logger.LogInformation("Observing {0} at {1}", streamMessage.Type,
                                                              streamMessage.Position);
                                        var command = new ProcessStreamMessage(streamMessage);
                                        await _messageChannel.Writer.WriteAsync(command, token).ConfigureAwait(false);
                                        await command.Completion.ConfigureAwait(false);
                                    }
                                    else if (streamMessage.Position % RecordPositionThreshold == 0 &&
                                             !_messagePumpCancellation.IsCancellationRequested)
                                    {
                                        await _messageChannel.Writer
                                        .WriteAsync(new RecordPosition(streamMessage), token)
                                        .ConfigureAwait(false);
                                    }
                                }, async(_, reason, exception) =>
                                {
                                    if (!_messagePumpCancellation.IsCancellationRequested)
                                    {
                                        await _messageChannel.Writer
                                        .WriteAsync(
                                            new SubscriptionDropped(reason, exception),
                                            _messagePumpCancellation.Token)
                                        .ConfigureAwait(false);
                                    }
                                },
                                    prefetchJsonData: false,
                                    name: "RoadRegistry.Editor.ProjectionHost.EventProcessor");

                                break;

                            case RecordPosition record:
                                try
                                {
                                    logger.LogInformation("Recording position of {MessageType} at {Position}.",
                                                          record.Message.Type, record.Message.Position);

                                    await using (var recordContext = dbContextFactory())
                                    {
                                        await recordContext
                                        .UpdateProjectionState(
                                            RoadRegistryEditorProjectionHost,
                                            record.Message.Position, _messagePumpCancellation.Token)
                                        .ConfigureAwait(false);
                                        await recordContext.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false);
                                    }
                                }
                                catch (Exception exception)
                                {
                                    logger.LogError(exception, exception.Message);
                                }

                                break;

                            case ProcessStreamMessage process:
                                try
                                {
                                    logger.LogInformation("Processing {MessageType} at {Position}",
                                                          process.Message.Type, process.Message.Position);

                                    var envelope = envelopeFactory.Create(process.Message);
                                    var handlers = resolver(envelope);
                                    await using (var processContext = dbContextFactory())
                                    {
                                        foreach (var handler in handlers)
                                        {
                                            await handler
                                            .Handler(processContext, envelope, _messagePumpCancellation.Token)
                                            .ConfigureAwait(false);
                                        }

                                        await processContext.UpdateProjectionState(
                                            RoadRegistryEditorProjectionHost,
                                            process.Message.Position,
                                            _messagePumpCancellation.Token).ConfigureAwait(false);
                                        await processContext.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false);
                                    }

                                    process.Complete();
                                }
                                catch (Exception exception)
                                {
                                    logger.LogError(exception, exception.Message);

                                    // how are we going to recover from this? do we even need to recover from this?
                                    // prediction: it's going to be a serialization error, a data quality error, or a bug

                                    process.Fault(exception);
                                }

                                break;

                            case SubscriptionDropped dropped:
                                if (dropped.Reason == SubscriptionDroppedReason.StreamStoreError)
                                {
                                    logger.LogError(dropped.Exception,
                                                    "Subscription was dropped because of a stream store error");
                                    await scheduler.Schedule(async token =>
                                    {
                                        if (!_messagePumpCancellation.IsCancellationRequested)
                                        {
                                            await _messageChannel.Writer.WriteAsync(new Resume(), token).ConfigureAwait(false);
                                        }
                                    }, ResubscribeAfter).ConfigureAwait(false);
                                }
                                else if (dropped.Reason == SubscriptionDroppedReason.SubscriberError)
                                {
                                    logger.LogError(dropped.Exception,
                                                    "Subscription was dropped because of a subscriber error");

                                    if (dropped.Exception != null &&
                                        dropped.Exception is SqlException sqlException &&
                                        sqlException.Number == -2 /* timeout */)
                                    {
                                        await scheduler.Schedule(async token =>
                                        {
                                            if (!_messagePumpCancellation.IsCancellationRequested)
                                            {
                                                await _messageChannel.Writer.WriteAsync(new Resume(), token).ConfigureAwait(false);
                                            }
                                        }, ResubscribeAfter).ConfigureAwait(false);
                                    }
                                }

                                break;
                            }
                        }
                    }
                }
                catch (TaskCanceledException)
                {
                    logger.LogInformation("EventProcessor message pump is exiting due to cancellation");
                }
                catch (OperationCanceledException)
                {
                    logger.LogInformation("EventProcessor message pump is exiting due to cancellation");
                }
                catch (Exception exception)
                {
                    logger.LogError(exception, "EventProcessor message pump is exiting due to a bug");
                }
                finally
                {
                    subscription?.Dispose();
                }
            }, _messagePumpCancellation.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
        }
 private ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages)
 {
     _resolver = resolver;
     _messages = messages;
 }
Beispiel #21
0
 public static IEnumerable<TestCaseData> ProjectMessageWithTokenCases()
 {
     var task = TaskFactory();
     //Match
     var token1 = new CancellationToken();
     var message1 = new object();
     var handler1 = new ConnectedProjectionHandler<CallRecordingConnection>(
         typeof(object),
         (connection, message, token) =>
         {
             connection.RecordCall(1, message, token);
             return task;
         });
     var resolver1 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new[] { handler1 });
     yield return new TestCaseData(
         resolver1,
         message1,
         token1,
         new[]
         {
             new Tuple<int, object, CancellationToken>(1, message1, token1)
         });
     //Mismatch
     var token2 = new CancellationToken();
     var message2 = new object();
     var resolver2 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new ConnectedProjectionHandler<CallRecordingConnection>[0]);
     yield return new TestCaseData(
         resolver2,
         message2,
         token2,
         new Tuple<int, object, CancellationToken>[0]);
     //Multimatch
     var token3 = new CancellationToken();
     var message3 = new object();
     var handler3 = new ConnectedProjectionHandler<CallRecordingConnection>(
         typeof(object),
         (connection, message, token) =>
         {
             connection.RecordCall(3, message, token);
             return task;
         });
     var handler4 = new ConnectedProjectionHandler<CallRecordingConnection>(
         typeof(object),
         (connection, message, token) =>
         {
             connection.RecordCall(4, message, token);
             return task;
         });
     var resolver3 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new[] { handler3, handler4 });
     yield return new TestCaseData(
         resolver3,
         message3,
         token3,
         new[]
         {
             new Tuple<int, object, CancellationToken>(3, message3, token3),
             new Tuple<int, object, CancellationToken>(4, message3, token3)
         });
 }