using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; namespace Cqrs.Tests.QueryDispatcherImplTests; public class Dispatch { public abstract class TestQueryHandler : IQueryHandler { public abstract ValueTask Handle(TestQuery query, CancellationToken cancellation); } public class TestQuery : IKnownQuery {} public class TestQueryResult {} [Theory] [InlineData(ServiceLifetime.Transient)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Singleton)] public async Task WhenCalled_ShouldExecuteHandler(ServiceLifetime lifetime) { // Given var testQuery = new TestQuery(); var testQueryResult = new TestQueryResult(); var queryHandler = Substitute.For(); queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(IQueryHandler), (IServiceProvider _) => queryHandler, lifetime ), ]; var services = serviceCollection.BuildServiceProvider(); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testQuery, CancellationToken.None); // Then result.ShouldBeSameAs(testQueryResult); await queryHandler.Received(1).Handle(testQuery, CancellationToken.None); } public class TestOpenBehavior : IDispatchBehavior where TRequest : notnull { private readonly Action _callback; public TestOpenBehavior(Action callback) { _callback = callback; } public ValueTask Handle(TRequest request, DispatchFurtherDelegate next, CancellationToken cancellationToken) { _callback.Invoke(request); return next(); } } [Fact] public async Task WhenPipelineConfigured_ShouldCallAllBehaviorsInOrder() { // Given var testQuery = new TestQuery(); var testQueryResult = new TestQueryResult(); List calls = []; var queryHandler = Substitute.For(); queryHandler.Handle(testQuery, CancellationToken.None) .Returns(testQueryResult) .AndDoes(_ => calls.Add("queryHandler")); var firstBehavior = Substitute.For>(); firstBehavior.Handle(testQuery, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("firstBehavior")); var secondBehavior = Substitute.For>(); secondBehavior.Handle(testQuery, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("secondBehavior")); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(IQueryHandler), (IServiceProvider _) => queryHandler, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior), (IServiceProvider _) => firstBehavior, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior), (IServiceProvider _) => secondBehavior, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior<,>), typeof(TestOpenBehavior<,>), ServiceLifetime.Transient ), ]; serviceCollection.AddTransient>(_ => (TestQuery _) => calls.Add("thirdBehavior")); var services = serviceCollection.BuildServiceProvider(); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testQuery, CancellationToken.None); // Then result.ShouldBeSameAs(testQueryResult); await firstBehavior.Received(1).Handle(testQuery, Arg.Any>(), Arg.Any()); await secondBehavior.Received(1).Handle(testQuery, Arg.Any>(), Arg.Any()); await queryHandler.Received(1).Handle(testQuery, CancellationToken.None); calls.ShouldBe(["firstBehavior", "secondBehavior", "thirdBehavior", "queryHandler"]); } [Fact] public async Task WhenNextIsNotCalled_ShouldStopExecutingPipeline() { // Given var testQuery = new TestQuery(); var testQueryResult = new TestQueryResult(); var testQueryResultAborted = new TestQueryResult(); List calls = []; var queryHandler = Substitute.For(); queryHandler.Handle(testQuery, CancellationToken.None) .Returns(testQueryResult) .AndDoes(_ => calls.Add("queryHandler")); var firstBehavior = Substitute.For>(); firstBehavior.Handle(testQuery, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("firstBehavior")); var secondBehavior = Substitute.For>(); secondBehavior.Handle(testQuery, Arg.Any>(), Arg.Any()) .Returns(args => ValueTask.FromResult(testQueryResultAborted)) .AndDoes(_ => calls.Add("secondBehavior")); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(IQueryHandler), (IServiceProvider _) => queryHandler, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior), (IServiceProvider _) => firstBehavior, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior), (IServiceProvider _) => secondBehavior, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehavior<,>), typeof(TestOpenBehavior<,>), ServiceLifetime.Transient ), ]; serviceCollection.AddTransient>(_ => (TestQuery _) => calls.Add("thirdBehavior")); var services = serviceCollection.BuildServiceProvider(); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testQuery, CancellationToken.None); // Then result.ShouldBeSameAs(testQueryResultAborted); await firstBehavior.Received(1).Handle(testQuery, Arg.Any>(), Arg.Any()); await secondBehavior.Received(1).Handle(testQuery, Arg.Any>(), Arg.Any()); await queryHandler.Received(0).Handle(testQuery, CancellationToken.None); calls.ShouldBe(["firstBehavior", "secondBehavior"]); } public abstract class AnotherTestQueryHandler : IQueryHandler { public abstract ValueTask Handle(TestQuery query, CancellationToken cancellation); } public class AnotherTestQueryResult {} [Fact] public async Task WhenTwoHandlersWithDifferentResultTypesRegisteredForOneQueryType_ShouldCorrectlyDispatchToBoth() // Fix to Cache Key Collision { // Given var testQuery = new TestQuery(); var testQueryResult = new TestQueryResult(); var anotherTestQueryResult = new AnotherTestQueryResult(); var queryHandler = Substitute.For(); queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult); var anotherQueryHandler = Substitute.For(); anotherQueryHandler.Handle(testQuery, CancellationToken.None).Returns(anotherTestQueryResult); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(IQueryHandler), (IServiceProvider _) => queryHandler, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IQueryHandler), (IServiceProvider _) => anotherQueryHandler, ServiceLifetime.Transient ), ]; var services = serviceCollection.BuildServiceProvider(); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testQuery, CancellationToken.None); var anotherResult = await sut.Dispatch(testQuery, CancellationToken.None); // Then result.ShouldBeSameAs(testQueryResult); await queryHandler.Received(1).Handle(testQuery, CancellationToken.None); anotherResult.ShouldBeSameAs(anotherTestQueryResult); await anotherQueryHandler.Received(1).Handle(testQuery, CancellationToken.None); } }