diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index c53f98c..01de3b6 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -4,6 +4,9 @@ on: push: tags: - '*.*.*' + pull_request: + branches: + - main jobs: build: @@ -23,18 +26,35 @@ jobs: with: dotnet-version: '9.x' + - name: Install Coverlet + run: dotnet tool install --global coverlet.console + - name: Restore run: dotnet restore src/DispatchR/DispatchR.csproj - name: Build run: dotnet build src/DispatchR/DispatchR.csproj --configuration Release --no-restore + - name: Run Tests + run: dotnet test --collect:"XPlat Code Coverage" + + - name: List files + run: ls -alh tests/DispatchR.IntegrationTest + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Extract version from tag id: get_version + if: startsWith(github.ref, 'refs/tags/v') run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Pack project + if: startsWith(github.ref, 'refs/tags/v') run: dotnet pack src/DispatchR/DispatchR.csproj --configuration Release --no-build -o ./nupkgs /p:PackageVersion=${{ steps.get_version.outputs.version }} - name: Push to NuGet + if: startsWith(github.ref, 'refs/tags/v') run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/DispatchR.sln b/DispatchR.sln index 075f584..ff93c34 100644 --- a/DispatchR.sln +++ b/DispatchR.sln @@ -37,6 +37,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.Service EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.ServiceB", "src\AspireModularExample\AspireModularSample.ServiceB\AspireModularSample.ServiceB.csproj", "{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.UnitTest", "tests\DispatchR.UnitTest\DispatchR.UnitTest.csproj", "{806030F5-86B1-4EFC-923C-94FF7D32DFC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.IntegrationTest", "tests\DispatchR.IntegrationTest\DispatchR.IntegrationTest.csproj", "{D8646A62-9FE7-4E79-861C-49391007F98A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.TestCommon", "tests\DispatchR.TestCommon\DispatchR.TestCommon.csproj", "{F01B6563-64D0-4316-947C-AB75426D9924}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +137,42 @@ Global {707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x64.Build.0 = Release|Any CPU {707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.ActiveCfg = Release|Any CPU {707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.Build.0 = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.Build.0 = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.Build.0 = Debug|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.Build.0 = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.ActiveCfg = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.Build.0 = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.ActiveCfg = Release|Any CPU + {806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.Build.0 = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.Build.0 = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.Build.0 = Debug|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.Build.0 = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.ActiveCfg = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.Build.0 = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.ActiveCfg = Release|Any CPU + {D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.Build.0 = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.ActiveCfg = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.Build.0 = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.ActiveCfg = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.Build.0 = Debug|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.Build.0 = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.ActiveCfg = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.Build.0 = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.ActiveCfg = Release|Any CPU + {F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,5 +188,8 @@ Global {3416F900-58F9-4AB6-AC8A-95B03C7BD9A3} = {BA3021C0-B64E-B700-D62A-004419E20C36} {7D2890FF-66F7-4870-BB89-952167AB0681} = {BA3021C0-B64E-B700-D62A-004419E20C36} {707E07BA-998C-49DE-BA56-7E9C0B6B7DBA} = {BA3021C0-B64E-B700-D62A-004419E20C36} + {806030F5-86B1-4EFC-923C-94FF7D32DFC9} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529} + {D8646A62-9FE7-4E79-861C-49391007F98A} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529} + {F01B6563-64D0-4316-947C-AB75426D9924} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 650572d..c755363 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # DispatchR 🚀 ![CI](https://github.com/hasanxdev/DispatchR/workflows/Release/badge.svg) +[![codecov](https://codecov.io/github/hasanxdev/dispatchr/graph/badge.svg?token=1FUG5DPUOE)](https://codecov.io/github/hasanxdev/dispatchr) [![NuGet](https://img.shields.io/nuget/dt/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator) [![NuGet](https://img.shields.io/nuget/vpre/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator) diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index 84c5c3b..110203b 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using Benchmark; using Benchmark.Notification; using Benchmark.SendRequest; @@ -19,33 +18,36 @@ .AddColumn(new OperationsColumn()) ); -public class OperationsColumn : IColumn +namespace Benchmark { - public string Id => nameof(OperationsColumn); - public string ColumnName => "OpsCount"; - public bool AlwaysShow => true; - public ColumnCategory Category => ColumnCategory.Custom; - public int PriorityInCategory => -10; - public bool IsNumeric => true; - public UnitType UnitType => UnitType.Dimensionless; - public string Legend => "Number of operations per invoke"; - - public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + public class OperationsColumn : IColumn { - return benchmarkCase.Descriptor.WorkloadMethod - .GetCustomAttributes(typeof(BenchmarkAttribute), false) - .Cast() - .FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1"; - } + public string Id => nameof(OperationsColumn); + public string ColumnName => "OpsCount"; + public bool AlwaysShow => true; + public ColumnCategory Category => ColumnCategory.Custom; + public int PriorityInCategory => -10; + public bool IsNumeric => true; + public UnitType UnitType => UnitType.Dimensionless; + public string Legend => "Number of operations per invoke"; - public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) - => GetValue(summary, benchmarkCase); + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + return benchmarkCase.Descriptor.WorkloadMethod + .GetCustomAttributes(typeof(BenchmarkAttribute), false) + .Cast() + .FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1"; + } - public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) - { - return true; - } + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + => GetValue(summary, benchmarkCase); + + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) + { + return true; + } - public bool IsAvailable(Summary summary) => true; - public bool IsDefault(Summary summary) => false; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary) => false; + } } \ No newline at end of file diff --git a/src/DispatchR/Configuration/ServiceRegistrator.cs b/src/DispatchR/Configuration/ServiceRegistrator.cs index f6e9a17..aa76b4f 100644 --- a/src/DispatchR/Configuration/ServiceRegistrator.cs +++ b/src/DispatchR/Configuration/ServiceRegistrator.cs @@ -53,12 +53,22 @@ public static void RegisterHandlers(IServiceCollection services, List allT .Where(p => { var interfaces = p.GetInterfaces(); + if (p.IsGenericType) + { + // handle generic pipelines + return interfaces + .FirstOrDefault(inter => + inter.IsGenericType && + inter.GetGenericTypeDefinition() == behaviorType) + ?.GetInterfaces().First().GetGenericTypeDefinition() == + handlerInterface.GetGenericTypeDefinition(); + } + return interfaces - .FirstOrDefault(inter => - inter.IsGenericType && - inter.GetGenericTypeDefinition() == behaviorType) - ?.GetInterfaces().First().GetGenericTypeDefinition() == - handlerInterface.GetGenericTypeDefinition(); + .FirstOrDefault(inter => + inter.IsGenericType && + inter.GetGenericTypeDefinition() == behaviorType) + ?.GetInterfaces().First() == handlerInterface; }).ToList(); // Sort pipelines by the specified order passed via ConfigurationOptions @@ -92,17 +102,28 @@ public static void RegisterHandlers(IServiceCollection services, List allT var responseTypeArg = handlerInterface.GenericTypeArguments[1]; if (genericHandlerResponseIsAwaitable && handlerResponseTypeIsAwaitable) { - if (genericHandlerResponseType.GetGenericTypeDefinition() != - handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition()) + var areGenericTypeArgumentsInHandlerInterfaceMismatched = + genericHandlerResponseType.IsGenericType && + handlerInterface.GenericTypeArguments[1].IsGenericType && + genericHandlerResponseType.GetGenericTypeDefinition() != + handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition(); + + if (areGenericTypeArgumentsInHandlerInterfaceMismatched || + genericHandlerResponseType.IsGenericType ^ + handlerInterface.GenericTypeArguments[1].IsGenericType) { continue; } // register async generic pipelines - responseTypeArg = responseTypeArg.GenericTypeArguments[0]; + if (responseTypeArg.GenericTypeArguments.Any()) + { + responseTypeArg = responseTypeArg.GenericTypeArguments[0]; + } } - var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0], responseTypeArg); + var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0], + responseTypeArg); services.AddKeyedScoped(typeof(IRequestHandler), key, closedGenericType); } else @@ -130,7 +151,8 @@ public static void RegisterHandlers(IServiceCollection services, List allT } } - public static void RegisterNotification(IServiceCollection services, List allTypes, Type syncNotificationHandlerType) + public static void RegisterNotification(IServiceCollection services, List allTypes, + Type syncNotificationHandlerType) { var allNotifications = allTypes .Where(p => @@ -140,7 +162,7 @@ public static void RegisterNotification(IServiceCollection services, List .Select(i => i.GetGenericTypeDefinition()) .Any(i => new[] { - syncNotificationHandlerType + syncNotificationHandlerType }.Contains(i)); }) .GroupBy(p => @@ -149,7 +171,7 @@ public static void RegisterNotification(IServiceCollection services, List .Where(i => i.IsGenericType) .First(i => new[] { - syncNotificationHandlerType + syncNotificationHandlerType }.Contains(i.GetGenericTypeDefinition())); return @interface.GenericTypeArguments.First(); }) @@ -172,7 +194,8 @@ private static bool IsAwaitable(Type type) if (type.IsGenericType) { var genericDef = type.GetGenericTypeDefinition(); - return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) || genericDef == typeof(IAsyncEnumerable<>); + return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) || + genericDef == typeof(IAsyncEnumerable<>); } return false; diff --git a/src/DispatchR/Exceptions/ExcludeHandlersCannotBeArrayEmptyException.cs b/src/DispatchR/Exceptions/ExcludeHandlersCannotBeArrayEmptyException.cs new file mode 100644 index 0000000..17aa435 --- /dev/null +++ b/src/DispatchR/Exceptions/ExcludeHandlersCannotBeArrayEmptyException.cs @@ -0,0 +1,5 @@ +namespace DispatchR.Exceptions; + +public class ExcludeHandlersCannotBeArrayEmptyException() : Exception("Exclude handlers cannot be array empty.") +{ +} \ No newline at end of file diff --git a/src/DispatchR/Exceptions/HandlerNotFoundException.cs b/src/DispatchR/Exceptions/HandlerNotFoundException.cs new file mode 100644 index 0000000..44f02d0 --- /dev/null +++ b/src/DispatchR/Exceptions/HandlerNotFoundException.cs @@ -0,0 +1,7 @@ +namespace DispatchR.Exceptions; + +public class HandlerNotFoundException() : Exception( + $""" + Handler for request of type '{typeof(TRequest).Name}' returning '{typeof(TResponse).Name}' was not found. + Make sure you have registered a handler that implements IRequestHandler<{typeof(TRequest).Name}, {typeof(TResponse).Name}> in the DI container. + """); \ No newline at end of file diff --git a/src/DispatchR/Exceptions/IncludeHandlersCannotBeArrayEmptyException.cs b/src/DispatchR/Exceptions/IncludeHandlersCannotBeArrayEmptyException.cs new file mode 100644 index 0000000..7a21310 --- /dev/null +++ b/src/DispatchR/Exceptions/IncludeHandlersCannotBeArrayEmptyException.cs @@ -0,0 +1,5 @@ +namespace DispatchR.Exceptions; + +public class IncludeHandlersCannotBeArrayEmptyException() : Exception("Include handlers cannot be array empty.") +{ +} \ No newline at end of file diff --git a/src/DispatchR/Extensions/ServiceCollectionExtensions.cs b/src/DispatchR/Extensions/ServiceCollectionExtensions.cs index 2fa6425..6f38de2 100644 --- a/src/DispatchR/Extensions/ServiceCollectionExtensions.cs +++ b/src/DispatchR/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using DispatchR.Requests.Stream; using Microsoft.Extensions.DependencyInjection; using System.Reflection; +using DispatchR.Exceptions; namespace DispatchR.Extensions; @@ -15,6 +16,16 @@ public static IServiceCollection AddDispatchR(this IServiceCollection services, var config = new ConfigurationOptions(); configuration(config); + if (config is {IncludeHandlers.Count:0}) + { + throw new IncludeHandlersCannotBeArrayEmptyException(); + } + + if (config is {ExcludeHandlers.Count:0}) + { + throw new ExcludeHandlersCannotBeArrayEmptyException(); + } + return services.AddDispatchR(config); } diff --git a/src/DispatchR/Requests/IMediator.cs b/src/DispatchR/Requests/IMediator.cs index 4519e93..e99043f 100644 --- a/src/DispatchR/Requests/IMediator.cs +++ b/src/DispatchR/Requests/IMediator.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using DispatchR.Exceptions; using DispatchR.Requests.Notification; using DispatchR.Requests.Send; using DispatchR.Requests.Stream; @@ -23,8 +24,15 @@ public sealed class Mediator(IServiceProvider serviceProvider) : IMediator public TResponse Send(IRequest request, CancellationToken cancellationToken) where TRequest : class, IRequest { - return serviceProvider.GetRequiredService>() - .Handle(Unsafe.As(request), cancellationToken); + try + { + return serviceProvider.GetRequiredService>() + .Handle(Unsafe.As(request), cancellationToken); + } + catch (Exception e) when (e.Message.Contains("No service for type", StringComparison.OrdinalIgnoreCase)) + { + throw new HandlerNotFoundException(); + } } public IAsyncEnumerable CreateStream(IStreamRequest request, diff --git a/src/DispatchR/Requests/Send/IRequestHandler.cs b/src/DispatchR/Requests/Send/IRequestHandler.cs index 6ce2084..ad6ad24 100644 --- a/src/DispatchR/Requests/Send/IRequestHandler.cs +++ b/src/DispatchR/Requests/Send/IRequestHandler.cs @@ -1,7 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + namespace DispatchR.Requests.Send; public interface IRequestHandler { + [ExcludeFromCodeCoverage] internal void SetNext(object handler) { } diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index 3bba68d..62d7caf 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -1,4 +1,5 @@ using DispatchR.Extensions; +using Sample; using Scalar.AspNetCore; using DispatchRNotificationSample = Sample.DispatchR.Notification; using DispatchRSample = Sample.DispatchR.SendRequest; @@ -125,7 +126,10 @@ app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +namespace Sample { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) + { + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } } \ No newline at end of file diff --git a/tests/DispatchR.IntegrationTest/DispatchR.IntegrationTest.csproj b/tests/DispatchR.IntegrationTest/DispatchR.IntegrationTest.csproj new file mode 100644 index 0000000..4594ed9 --- /dev/null +++ b/tests/DispatchR.IntegrationTest/DispatchR.IntegrationTest.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + enable + enable + false + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/tests/DispatchR.IntegrationTest/NotificationTests.cs b/tests/DispatchR.IntegrationTest/NotificationTests.cs new file mode 100644 index 0000000..477a342 --- /dev/null +++ b/tests/DispatchR.IntegrationTest/NotificationTests.cs @@ -0,0 +1,51 @@ +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.Requests.Notification; +using DispatchR.Requests.Send; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.Notification; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Moq; + +namespace DispatchR.IntegrationTest; + +public class NotificationTests +{ + [Fact] + public async Task Publish_CallsAllHandlers_WhenMultipleHandlersAreRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + + var spyPipelineOneMock = new Mock>(); + var spyPipelineTwoMock = new Mock>(); + var spyPipelineThreeMock = new Mock>(); + + spyPipelineOneMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())); + spyPipelineTwoMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())); + spyPipelineThreeMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())); + + services.AddScoped>(sp => spyPipelineOneMock.Object); + services.AddScoped>(sp => spyPipelineTwoMock.Object); + services.AddScoped>(sp => spyPipelineThreeMock.Object); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert + spyPipelineOneMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + spyPipelineTwoMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + spyPipelineThreeMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } +} \ No newline at end of file diff --git a/tests/DispatchR.IntegrationTest/RequestHandlerTests.cs b/tests/DispatchR.IntegrationTest/RequestHandlerTests.cs new file mode 100644 index 0000000..746de8d --- /dev/null +++ b/tests/DispatchR.IntegrationTest/RequestHandlerTests.cs @@ -0,0 +1,60 @@ +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.Requests.Send; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.SendRequest.Sync; +using DispatchR.TestCommon.Fixtures.SendRequest.Task; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Moq; + +namespace DispatchR.IntegrationTest; + +public class RequestHandlerTests +{ + [Fact] + public async Task Send_UsesPipelineBehaviors_RequestWithSinglePipelines() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingValueTaskHandler)]; + }); + + var firstPipeline = services + .Single(p => p.IsKeyedService && p.KeyedImplementationType == typeof(PingValueTaskFirstPipelineBehavior)); + var secondPipeline = services + .Single(p => p.IsKeyedService && p.KeyedImplementationType == typeof(PingValueTaskSecondPipelineBehavior)); + + var spyPipelineTwoMock = new Mock>>(); + spyPipelineTwoMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns((PingValueTask req, CancellationToken ct) => new PingValueTaskHandler().Handle(req, ct)); + + var spyPipelineOneMock = new Mock>>(); + spyPipelineOneMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns((PingValueTask req, CancellationToken ct) => spyPipelineTwoMock.Object.Handle(req, ct)); + + services.RemoveAllKeyed(typeof(IRequestHandler), firstPipeline.ServiceKey); + services.RemoveAllKeyed(typeof(IRequestHandler), secondPipeline.ServiceKey); + + services.AddKeyedScoped(typeof(IRequestHandler), firstPipeline.ServiceKey!, (_,__) => spyPipelineTwoMock.Object); + services.AddKeyedScoped(typeof(IRequestHandler), secondPipeline.ServiceKey!, (_,__) => spyPipelineOneMock.Object); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + + // Act + var result = await mediator.Send(new PingValueTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + spyPipelineOneMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + spyPipelineTwoMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } +} \ No newline at end of file diff --git a/tests/DispatchR.IntegrationTest/StreamRequestHandlerTests.cs b/tests/DispatchR.IntegrationTest/StreamRequestHandlerTests.cs new file mode 100644 index 0000000..181d239 --- /dev/null +++ b/tests/DispatchR.IntegrationTest/StreamRequestHandlerTests.cs @@ -0,0 +1,56 @@ +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.Requests.Send; +using DispatchR.Requests.Stream; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.SendRequest.Sync; +using DispatchR.TestCommon.Fixtures.SendRequest.Task; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using DispatchR.TestCommon.Fixtures.StreamRequest; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Moq; + +namespace DispatchR.IntegrationTest; + +public class StreamRequestHandlerTests +{ + [Fact] + public async Task CreateStream_UsesPipelineBehavior_WithSinglePipeline() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyStreamHandler.GetType()]; + }); + + var firstPipeline = services + .Single(p => p.IsKeyedService && p.KeyedImplementationType == typeof(CounterPipelineStreamHandler)); + + var spyPipelineOneMock = new Mock>(); + spyPipelineOneMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns((CounterStreamRequest req, CancellationToken ct) => Fixture.AnyStreamHandler.Handle(req, ct)); + + services.RemoveAllKeyed(typeof(IRequestHandler), firstPipeline.ServiceKey); + + services.AddKeyedScoped(typeof(IRequestHandler), firstPipeline.ServiceKey!, (_,__) => spyPipelineOneMock.Object); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + int counter = 0; + await foreach (var response in mediator.CreateStream(Fixture.AnyStreamRequest, CancellationToken.None)) + { + counter++; + } + + // Assert + Assert.Equal(1, counter); + spyPipelineOneMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/DispatchR.TestCommon.csproj b/tests/DispatchR.TestCommon/DispatchR.TestCommon.csproj new file mode 100644 index 0000000..bb8e64a --- /dev/null +++ b/tests/DispatchR.TestCommon/DispatchR.TestCommon.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + diff --git a/tests/DispatchR.TestCommon/Fixtures/Fixture.cs b/tests/DispatchR.TestCommon/Fixtures/Fixture.cs new file mode 100644 index 0000000..d4ff657 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Fixture.cs @@ -0,0 +1,19 @@ +using DispatchR.TestCommon.Fixtures.SendRequest.Task; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTaskWithOutResponse; +using DispatchR.TestCommon.Fixtures.StreamRequest; + +namespace DispatchR.TestCommon.Fixtures; + +public class Fixture +{ + public static PingTaskHandler AnyHandlerRequestWithoutPipeline => new(); + + public static PingValueTaskHandler AnyHandlerRequestWithPipeline => new(); + + public static PingValueTaskWithoutResponse AnyRequestWithoutResponsePipeline => new(); + public static PingValueTaskWithoutResponseHandler AnyHandlerRequestWithoutResponseWithPipeline => new(); + + public static CounterStreamRequest AnyStreamRequest => new(); + public static CounterStreamHandler AnyStreamHandler => new(); +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/MultiHandlersNotification.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/MultiHandlersNotification.cs new file mode 100644 index 0000000..70195cc --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/MultiHandlersNotification.cs @@ -0,0 +1,5 @@ +using DispatchR.Requests.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed record MultiHandlersNotification(Guid Id) : INotification; \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationOneHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationOneHandler.cs new file mode 100644 index 0000000..284de16 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationOneHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class NotificationOneHandler() : INotificationHandler +{ + public async ValueTask Handle(MultiHandlersNotification request, CancellationToken cancellationToken) + { + await Task.Delay(100, cancellationToken); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationThreeHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationThreeHandler.cs new file mode 100644 index 0000000..3da0fa0 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationThreeHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class NotificationThreeHandler() : INotificationHandler +{ + public ValueTask Handle(MultiHandlersNotification request, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationTwoHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationTwoHandler.cs new file mode 100644 index 0000000..8750f9c --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/NotificationTwoHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class NotificationTwoHandler() : INotificationHandler +{ + public ValueTask Handle(MultiHandlersNotification request, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithResponse.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithResponse.cs new file mode 100644 index 0000000..a34bef2 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithResponse.cs @@ -0,0 +1,15 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest; + +public class GenericPipelineBehaviorWithResponse() + : IPipelineBehavior> + where TRequest : class, IRequest>, new() +{ + public ValueTask Handle(TRequest request, CancellationToken cancellationToken) + { + return NextPipeline.Handle(request, cancellationToken); + } + + public required IRequestHandler> NextPipeline { get; set; } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithoutResponse.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithoutResponse.cs new file mode 100644 index 0000000..419dcc2 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/GenericPipelineBehaviorWithoutResponse.cs @@ -0,0 +1,15 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest; + +public class GenericPipelineBehaviorWithOutResponse() + : IPipelineBehavior + where TRequest : class, IRequest, new() +{ + public System.Threading.Tasks.ValueTask Handle(TRequest request, CancellationToken cancellationToken) + { + return NextPipeline.Handle(request, cancellationToken); + } + + public required IRequestHandler NextPipeline { get; set; } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/RequestWithoutHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/RequestWithoutHandler.cs new file mode 100644 index 0000000..8fe8b44 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/RequestWithoutHandler.cs @@ -0,0 +1,7 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest; + +public class RequestWithoutHandler : IRequest +{ +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetime.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetime.cs new file mode 100644 index 0000000..b1ed25c --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetime.cs @@ -0,0 +1,8 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ReusedInScopedLifetime; + +public class RequestReusedInScopedLifetime : IRequest +{ + +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetimeHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetimeHandler.cs new file mode 100644 index 0000000..d5c00a7 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ReusedInScopedLifetime/RequestReusedInScopedLifetimeHandler.cs @@ -0,0 +1,12 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ReusedInScopedLifetime; + +public class RequestReusedInScopedLifetimeHandler : IRequestHandler +{ + private int _counter = 0; + public int Handle(RequestReusedInScopedLifetime request, CancellationToken cancellationToken) + { + return _counter++; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/Ping.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/Ping.cs new file mode 100644 index 0000000..5bf74c0 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/Ping.cs @@ -0,0 +1,8 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.Sync; + +public class Ping : IRequest +{ + +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/PingHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/PingHandler.cs new file mode 100644 index 0000000..0052566 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Sync/PingHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.Sync; + +public class PingHandler() : IRequestHandler +{ + public int Handle(Ping request, CancellationToken cancellationToken) + { + return 1; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTask.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTask.cs new file mode 100644 index 0000000..4f24a9c --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTask.cs @@ -0,0 +1,8 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.Task; + +public class PingTask : IRequest> +{ + +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTaskHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTaskHandler.cs new file mode 100644 index 0000000..0a30bb3 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/Task/PingTaskHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.Task; + +public class PingTaskHandler() : IRequestHandler> +{ + public Task Handle(PingTask request, CancellationToken cancellationToken) + { + return System.Threading.Tasks.Task.FromResult(1); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTask.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTask.cs new file mode 100644 index 0000000..a5dab0b --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTask.cs @@ -0,0 +1,8 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; + +public class PingValueTask : IRequest> +{ + +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskFirstPipelineBehavior.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskFirstPipelineBehavior.cs new file mode 100644 index 0000000..10a1b80 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskFirstPipelineBehavior.cs @@ -0,0 +1,15 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; + +public class PingValueTaskFirstPipelineBehavior() : IPipelineBehavior> +{ + public static DateTime ExecutionTime { get; private set; } + public required IRequestHandler> NextPipeline { get; set; } + public async ValueTask Handle(PingValueTask request, CancellationToken cancellationToken) + { + ExecutionTime = DateTime.Now; + await System.Threading.Tasks.Task.Delay(100, cancellationToken).ConfigureAwait(false); + return await NextPipeline.Handle(request, cancellationToken); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskHandler.cs new file mode 100644 index 0000000..75bccf2 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; + +public class PingValueTaskHandler() : IRequestHandler> +{ + public ValueTask Handle(PingValueTask request, CancellationToken cancellationToken) + { + return System.Threading.Tasks.ValueTask.FromResult(1); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskSecondPipelineBehavior.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskSecondPipelineBehavior.cs new file mode 100644 index 0000000..62c0ecc --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTask/PingValueTaskSecondPipelineBehavior.cs @@ -0,0 +1,16 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTask +{ + public class PingValueTaskSecondPipelineBehavior() : IPipelineBehavior> + { + public static DateTime ExecutionTime { get; private set; } + public required IRequestHandler> NextPipeline { get; set; } + + public ValueTask Handle(PingValueTask request, CancellationToken cancellationToken) + { + ExecutionTime = DateTime.Now; + return NextPipeline.Handle(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTask.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTask.cs new file mode 100644 index 0000000..31ff982 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTask.cs @@ -0,0 +1,7 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTaskWithOutResponse; + +public class PingValueTaskWithoutResponse : IRequest +{ +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskFirstPipelineBehavior.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskFirstPipelineBehavior.cs new file mode 100644 index 0000000..584f206 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskFirstPipelineBehavior.cs @@ -0,0 +1,15 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTaskWithOutResponse; + +public class PingValueTaskFirstPipelineBehavior() : IPipelineBehavior +{ + public static DateTime ExecutionTime { get; private set; } + public required IRequestHandler NextPipeline { get; set; } + public async System.Threading.Tasks.ValueTask Handle(PingValueTaskWithoutResponse request, CancellationToken cancellationToken) + { + ExecutionTime = DateTime.Now; + await System.Threading.Tasks.Task.Delay(100, cancellationToken).ConfigureAwait(false); + await NextPipeline.Handle(request, cancellationToken); + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskHandler.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskHandler.cs new file mode 100644 index 0000000..9b54119 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTaskWithOutResponse; + +public class PingValueTaskWithoutResponseHandler() : IRequestHandler +{ + public System.Threading.Tasks.ValueTask Handle(PingValueTaskWithoutResponse request, CancellationToken cancellationToken) + { + return System.Threading.Tasks.ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskSecondPipelineBehavior.cs b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskSecondPipelineBehavior.cs new file mode 100644 index 0000000..f04f92e --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/SendRequest/ValueTaskWithOutResponse/PingValueTaskSecondPipelineBehavior.cs @@ -0,0 +1,16 @@ +using DispatchR.Requests.Send; + +namespace DispatchR.TestCommon.Fixtures.SendRequest.ValueTaskWithOutResponse +{ + public class PingValueTaskWithoutResponseSecondPipelineBehavior() : IPipelineBehavior + { + public static DateTime ExecutionTime { get; private set; } + public required IRequestHandler NextPipeline { get; set; } + + public System.Threading.Tasks.ValueTask Handle(PingValueTaskWithoutResponse request, CancellationToken cancellationToken) + { + ExecutionTime = DateTime.Now; + return NextPipeline.Handle(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterPipelineStreamHandler.cs b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterPipelineStreamHandler.cs new file mode 100644 index 0000000..4f3b980 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterPipelineStreamHandler.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; +using DispatchR.Requests.Stream; + +namespace DispatchR.TestCommon.Fixtures.StreamRequest; + +public class CounterPipelineStreamHandler : IStreamPipelineBehavior +{ + public required IStreamRequestHandler NextPipeline { get; set; } + + public async IAsyncEnumerable Handle(CounterStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var response in NextPipeline.Handle(request, cancellationToken).ConfigureAwait(false)) + { + yield return response; + } + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamHandler.cs b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamHandler.cs new file mode 100644 index 0000000..7da1265 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamHandler.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using DispatchR.Requests.Stream; + +namespace DispatchR.TestCommon.Fixtures.StreamRequest; + +public class CounterStreamHandler() : IStreamRequestHandler +{ + public async IAsyncEnumerable Handle(CounterStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.CompletedTask; + yield return string.Empty; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamRequest.cs b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamRequest.cs new file mode 100644 index 0000000..7111fac --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/CounterStreamRequest.cs @@ -0,0 +1,5 @@ +using DispatchR.Requests.Stream; + +namespace DispatchR.TestCommon.Fixtures.StreamRequest; + +public class CounterStreamRequest : IStreamRequest { } \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/StreamRequest/GenericPipelineBehavior.cs b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/GenericPipelineBehavior.cs new file mode 100644 index 0000000..1e07078 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/StreamRequest/GenericPipelineBehavior.cs @@ -0,0 +1,18 @@ +using DispatchR.Requests.Stream; + +namespace DispatchR.TestCommon.Fixtures.StreamRequest; + +public class GenericPipelineBehavior() + : IStreamPipelineBehavior + where TRequest : class, IStreamRequest, new() +{ + public async IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken) + { + await foreach (var response in NextPipeline.Handle(request, cancellationToken).ConfigureAwait(false)) + { + yield return response; + } + } + + public IStreamRequestHandler NextPipeline { get; set; } +} \ No newline at end of file diff --git a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs new file mode 100644 index 0000000..f8c3b25 --- /dev/null +++ b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs @@ -0,0 +1,241 @@ +using DispatchR.Exceptions; +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.Requests.Stream; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.Notification; +using DispatchR.TestCommon.Fixtures.SendRequest; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using Microsoft.Extensions.DependencyInjection; + +namespace DispatchR.UnitTest; + +public class AddDispatchRConfigurationTests +{ + [Fact] + public void TraditionalAddDispatchR_ReturnsExpectedResponse_DefaultHandlers() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(typeof(Fixture).Assembly, withPipelines: true, withNotifications: false); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType!.GetInterface(typeof(IStreamRequestHandler<,>).Name, true) is null); + Assert.True(countOfAllSimpleHandlers > 1); + } + + [Fact] + public void AddDispatchR_ReturnsExpectedResponse_DefaultHandlers() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = null; // <== this line + cfg.ExcludeHandlers = null; // <== this line + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType!.GetInterface(typeof(IStreamRequestHandler<,>).Name, true) is null); + Assert.True(countOfAllSimpleHandlers > 1); + } + + [Fact] + public void AddDispatchR_ReturnsExpectedResponse_IncludeSingleHandler() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithoutPipeline.GetType()]; // <== this line + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType!.GetInterface(typeof(IStreamRequestHandler<,>).Name, true) is null); + Assert.Equal(1, countOfAllSimpleHandlers); + } + + [Fact] + public void AddDispatchR_ReturnsExpectedResponse_ExcludeSingleHandler() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.ExcludeHandlers = [Fixture.AnyHandlerRequestWithoutPipeline.GetType()]; // <== this line + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType == Fixture.AnyHandlerRequestWithoutPipeline.GetType()); + Assert.Equal(0, countOfAllSimpleHandlers); + } + + [Fact] + public void AddDispatchR_ReturnsExpectedResponse_IncludeAndExcludeOneHandlers() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithoutPipeline.GetType()]; + cfg.ExcludeHandlers = [Fixture.AnyHandlerRequestWithoutPipeline.GetType()]; + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType == Fixture.AnyHandlerRequestWithoutPipeline.GetType()); + Assert.Equal(0, countOfAllSimpleHandlers); + } + + [Fact] + public void AddDispatchR_ThrowsException_WhenIncludeHandlersBeEmpty() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var act = () => services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = []; + }); + + // Assert + Assert.Throws(act); + } + + [Fact] + public void AddDispatchR_ThrowsException_WhenExcludeHandlersBeEmpty() + { + // Arrange + var services = new ServiceCollection(); + + // Act + var act = () => services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.ExcludeHandlers = []; + }); + + // Assert + Assert.Throws(act); + } + + [Fact] + public async Task AddDispatchR_UsesPipelineBehaviorsInCorrectOrder_RequestWithMultiplePipelines() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.PipelineOrder = + [ + typeof(PingValueTaskFirstPipelineBehavior), + typeof(PingValueTaskSecondPipelineBehavior), + ]; + cfg.IncludeHandlers = [typeof(PingValueTaskHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingValueTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + Assert.True(PingValueTaskFirstPipelineBehavior.ExecutionTime < PingValueTaskSecondPipelineBehavior.ExecutionTime); + } + + [Fact] + public void AddDispatchR_RegisterGenericPipeline_IncludeGenericPipeline() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithPipeline.GetType()]; + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService && + p.KeyedImplementationType!.IsGenericType && + p.KeyedImplementationType?.GetGenericTypeDefinition() == typeof(GenericPipelineBehaviorWithResponse<,>).GetGenericTypeDefinition()); + Assert.Equal(1, countOfAllSimpleHandlers); + } + + [Fact] + public void AddDispatchR_RegisterNotifications_FindNotifications() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithoutPipeline.GetType()]; + }); + + // Assert + var countOfAllSimpleHandlers = services + .Count(p => + p.IsKeyedService is false && + (p.ImplementationType == typeof(NotificationOneHandler) || + p.ImplementationType == typeof(NotificationTwoHandler) || + p.ImplementationType == typeof(NotificationThreeHandler))); + + Assert.Equal(3, countOfAllSimpleHandlers); + } +} \ No newline at end of file diff --git a/tests/DispatchR.UnitTest/DispatchR.UnitTest.csproj b/tests/DispatchR.UnitTest/DispatchR.UnitTest.csproj new file mode 100644 index 0000000..69524d7 --- /dev/null +++ b/tests/DispatchR.UnitTest/DispatchR.UnitTest.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/DispatchR.UnitTest/RequestHandlerTests.cs b/tests/DispatchR.UnitTest/RequestHandlerTests.cs new file mode 100644 index 0000000..fb78e28 --- /dev/null +++ b/tests/DispatchR.UnitTest/RequestHandlerTests.cs @@ -0,0 +1,205 @@ +using DispatchR.Configuration; +using DispatchR.Exceptions; +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.SendRequest; +using DispatchR.TestCommon.Fixtures.SendRequest.ReusedInScopedLifetime; +using DispatchR.TestCommon.Fixtures.SendRequest.Sync; +using DispatchR.TestCommon.Fixtures.SendRequest.Task; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace DispatchR.UnitTest; + +public class RequestHandlerTests +{ + [Fact] + public void Send_ReturnsExpectedResponse_SyncRequestHandler() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = mediator.Send(new Ping(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithTask() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingTaskHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithValueTask() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingValueTaskHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingValueTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task Send_UsesPipelineBehaviors_RequestWithPipelines() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingValueTaskHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingValueTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task Send_UsesPipelineBehaviors_RequestWithOutResponseWithPipelines() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithoutResponseWithPipeline.GetType()]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Send(Fixture.AnyRequestWithoutResponsePipeline, CancellationToken.None); + + // Assert + // Just checking if it runs without exceptions + } + + [Fact] + public async Task Send_UsesPipelineBehaviors_ChangePipelineOrdering() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.PipelineOrder = + [ + typeof(PingValueTaskFirstPipelineBehavior) + ]; + cfg.IncludeHandlers = [Fixture.AnyHandlerRequestWithPipeline.GetType()]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingValueTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Send_ThrowsException_WhenNoHandlerIsRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(RequestWithoutHandler)]; + }); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + void Action() => mediator.Send(new RequestWithoutHandler(), CancellationToken.None); + + // Assert + var exception = Assert.Throws>(Action); + Assert.Equal(""" + Handler for request of type 'RequestWithoutHandler' returning 'Int32' was not found. + Make sure you have registered a handler that implements IRequestHandler in the DI container. + """, exception.Message); + } + + [Fact] + public void Send_UsesCachedHandler_InstanceReusedInScopedLifetime() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(RequestReusedInScopedLifetimeHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var scope = serviceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var first = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); + var second = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); + var third = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); + + // Assert + Assert.Equal(3, first + second + third); + } +} \ No newline at end of file diff --git a/tests/DispatchR.UnitTest/StreamRequestHandlerTests.cs b/tests/DispatchR.UnitTest/StreamRequestHandlerTests.cs new file mode 100644 index 0000000..8829949 --- /dev/null +++ b/tests/DispatchR.UnitTest/StreamRequestHandlerTests.cs @@ -0,0 +1,69 @@ +using DispatchR.Configuration; +using DispatchR.Exceptions; +using DispatchR.Extensions; +using DispatchR.Requests; +using DispatchR.TestCommon.Fixtures; +using DispatchR.TestCommon.Fixtures.SendRequest; +using DispatchR.TestCommon.Fixtures.SendRequest.ReusedInScopedLifetime; +using DispatchR.TestCommon.Fixtures.SendRequest.Sync; +using DispatchR.TestCommon.Fixtures.SendRequest.Task; +using DispatchR.TestCommon.Fixtures.SendRequest.ValueTask; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace DispatchR.UnitTest; + +public class StreamRequestHandlerTests +{ + [Fact] + public async Task CreaseStream_StreamHandlerReturnsExpectedResponse_WhenSingleHandlerIsIncluded() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyStreamHandler.GetType()]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + int counter = 0; + await foreach (var response in mediator.CreateStream(Fixture.AnyStreamRequest, CancellationToken.None)) + { + counter++; + } + + // Assert + Assert.Equal(1, counter); + } + + [Fact] + public async Task Send_UsesPipelineBehaviors_RequestWithPipelines() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [Fixture.AnyStreamHandler.GetType()]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + int counter = 0; + await foreach (var response in mediator.CreateStream(Fixture.AnyStreamRequest, CancellationToken.None)) + { + counter++; + } + + // Assert + Assert.Equal(1, counter); + } +} \ No newline at end of file