using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; namespace Cqrs.Tests.CommandDispatcherImplTests; public class Dispatch { public class TestCommand : IKnownCommand {} public class TestCommandResult {} [Theory] [InlineData(ServiceLifetime.Transient)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Singleton)] public async Task WhenCalled_ShouldExecuteHandler(ServiceLifetime lifetime) { // Given var testCommand = new TestCommand(); var testCommandResult = new TestCommandResult(); var commandHandler = Substitute.For>(); commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(ICommandHandler), (IServiceProvider _) => commandHandler, lifetime ), ]; var services = serviceCollection.BuildServiceProvider(); var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testCommand, CancellationToken.None); // Then result.ShouldBeSameAs(testCommandResult); await commandHandler.Received(1).Handle(testCommand, CancellationToken.None); } public class TestOpenBehaviour : IDispatchBehaviour where TRequest : notnull { private readonly Action _callback; public TestOpenBehaviour(Action callback) { _callback = callback; } public ValueTask Handle(TRequest request, DispatchFurtherDelegate next, CancellationToken cancellationToken) { _callback.Invoke(request); return next(); } } [Fact] public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder() { // Given var testCommand = new TestCommand(); var testCommandResult = new TestCommandResult(); List calls = []; var commandHandler = Substitute.For>(); commandHandler.Handle(testCommand, CancellationToken.None) .Returns(testCommandResult) .AndDoes(_ => calls.Add("commandHandler")); var firstBehaviour = Substitute.For>(); firstBehaviour.Handle(testCommand, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("firstBehaviour")); var secondBehaviour = Substitute.For>(); secondBehaviour.Handle(testCommand, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("secondBehaviour")); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(ICommandHandler), (IServiceProvider _) => commandHandler, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour), (IServiceProvider _) => firstBehaviour, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour), (IServiceProvider _) => secondBehaviour, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour<,>), typeof(TestOpenBehaviour<,>), ServiceLifetime.Transient ), ]; serviceCollection.AddTransient>(_ => (TestCommand _) => calls.Add("thirdBehaviour")); var services = serviceCollection.BuildServiceProvider(); var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testCommand, CancellationToken.None); // Then result.ShouldBeSameAs(testCommandResult); await firstBehaviour.Received(1).Handle(testCommand, Arg.Any>(), Arg.Any()); await secondBehaviour.Received(1).Handle(testCommand, Arg.Any>(), Arg.Any()); await commandHandler.Received(1).Handle(testCommand, CancellationToken.None); calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "commandHandler"]); } [Fact] public async Task WhenNextIsNotCalled_ShouldStopExecutingPipeline() { // Given var testCommand = new TestCommand(); var testCommandResult = new TestCommandResult(); var testCommandResultAborted = new TestCommandResult(); List calls = []; var commandHandler = Substitute.For>(); commandHandler.Handle(testCommand, CancellationToken.None) .Returns(testCommandResult) .AndDoes(_ => calls.Add("commandHandler")); var firstBehaviour = Substitute.For>(); firstBehaviour.Handle(testCommand, Arg.Any>(), Arg.Any()) .Returns(args => ((DispatchFurtherDelegate)args[1]).Invoke()) .AndDoes(_ => calls.Add("firstBehaviour")); var secondBehaviour = Substitute.For>(); secondBehaviour.Handle(testCommand, Arg.Any>(), Arg.Any()) .Returns(args => ValueTask.FromResult(testCommandResultAborted)) .AndDoes(_ => calls.Add("secondBehaviour")); ServiceCollection serviceCollection = [ new ServiceDescriptor( typeof(ICommandHandler), (IServiceProvider _) => commandHandler, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour), (IServiceProvider _) => firstBehaviour, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour), (IServiceProvider _) => secondBehaviour, ServiceLifetime.Transient ), new ServiceDescriptor( typeof(IDispatchBehaviour<,>), typeof(TestOpenBehaviour<,>), ServiceLifetime.Transient ), ]; serviceCollection.AddTransient>(_ => (TestCommand _) => calls.Add("thirdBehaviour")); var services = serviceCollection.BuildServiceProvider(); var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache()); // When var result = await sut.Dispatch(testCommand, CancellationToken.None); // Then result.ShouldBeSameAs(testCommandResultAborted); await firstBehaviour.Received(1).Handle(testCommand, Arg.Any>(), Arg.Any()); await secondBehaviour.Received(1).Handle(testCommand, Arg.Any>(), Arg.Any()); await commandHandler.Received(0).Handle(testCommand, CancellationToken.None); calls.ShouldBe(["firstBehaviour", "secondBehaviour"]); } }