From e5fbb7ce2e1f9b9760aada4327cf9a1a49431204 Mon Sep 17 00:00:00 2001 From: Kamran Sadin Date: Sun, 1 Jun 2025 19:45:28 +0330 Subject: [PATCH 1/4] Simple CI-CD --- .github/workflows/publish-nuget.yml | 27 ++++----------------------- src/KSFramework/KSFramework.csproj | 4 ++-- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 70cc703..f56c699 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -4,8 +4,6 @@ on: push: branches: - main - tags: - - 'v*.*.*' jobs: publish: @@ -15,20 +13,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Setup .NET SDKs + - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 8.0.x - 10.0.100-preview.4.25258.110 - - - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.11.0 - with: - versionSpec: '5.12.0' - - - name: Use GitVersion - uses: gittools/actions/gitversion/execute@v0.11.0 + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore @@ -37,14 +25,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Pack NuGet - run: | - dotnet pack --configuration Release \ - -p:PackageVersion=${{ steps.gitversion.outputs.NuGetVersionV2 }} \ - --no-build -o out + run: dotnet pack --configuration Release --no-build -o out - name: Push to NuGet - if: success() - run: | - dotnet nuget push out/*.nupkg \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --source https://api.nuget.org/v3/index.json + run: dotnet nuget push out/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json diff --git a/src/KSFramework/KSFramework.csproj b/src/KSFramework/KSFramework.csproj index 676ad05..14b52c3 100644 --- a/src/KSFramework/KSFramework.csproj +++ b/src/KSFramework/KSFramework.csproj @@ -1,14 +1,14 @@  - net8.0;net10.0 enable enable + net8.0 KSFramework - 1.0.0 + 1.0.20 Kamran Sadin Sadin Copyright (c) 2022 SadinCo. From 2157bd8bebd1046bed9de9b72798e346f1834369 Mon Sep 17 00:00:00 2001 From: Kamran Sadin Date: Mon, 2 Jun 2025 10:41:48 +0330 Subject: [PATCH 2/4] Fix yaml file issue --- .github/workflows/publish-nuget.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 7cb4da3..076a206 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -29,12 +29,7 @@ jobs: run: dotnet build KSFramework.sln --configuration Release --no-restore - name: Pack NuGet - run: dotnet pack src/KSFramework/KSFramework.csproj \ - --configuration Release \ - -p:PackageVersion=${{ env.PACKAGE_VERSION }} \ - --no-build -o out + run: dotnet pack src/KSFramework/KSFramework.csproj --configuration Release -p:PackageVersion=${{ env.PACKAGE_VERSION }} --no-build -o out - name: Push to NuGet - run: dotnet nuget push out/*.nupkg \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --source https://api.nuget.org/v3/index.json + run: dotnet nuget push out/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json From 916d2bbb0fe8e388701ed160878f13f40aaa2fb0 Mon Sep 17 00:00:00 2001 From: Kamran Sadin Date: Mon, 2 Jun 2025 23:05:53 +0330 Subject: [PATCH 3/4] Mediator base features --- .../MediatorSampleApp/CounterStreamTest.cs | 6 ++++++ .../LoggingNotificationBehavior.cs | 6 ++++++ .../MediatorSampleApp/MultiplyByTwoRequest.cs | 6 ++++++ Samples/MediatorSampleApp/NotificationTest.cs | 6 ++++++ .../Messaging/Abstraction/IMediator.cs | 19 +++++++++++++++++++ .../Messaging/Abstraction/INotification.cs | 6 ++++++ .../Abstraction/INotificationBehavior.cs | 6 ++++++ .../Abstraction/INotificationHandler.cs | 6 ++++++ .../Abstraction/IPipelineBehavior.cs | 6 ++++++ .../Messaging/Abstraction/IPostProcessor.cs | 6 ++++++ .../Messaging/Abstraction/IRequest.cs | 5 +++++ .../Messaging/Abstraction/IRequestHandler.cs | 7 +++++++ .../Abstraction/IRequestPreProcessor.cs | 6 ++++++ .../Messaging/Abstraction/ISender.cs | 16 ++++++++++++++++ .../Messaging/Abstraction/IStreamRequest.cs | 6 ++++++ .../Abstraction/IStreamRequestHandler.cs | 6 ++++++ src/KSFramework/Messaging/Abstraction/Unit.cs | 6 ++++++ .../Messaging/Behaviors/LoggingBehavior.cs | 6 ++++++ .../Behaviors/LoggingPostProcessor.cs | 6 ++++++ .../Behaviors/LoggingPreProcessor.cs | 10 ++++++++++ .../Behaviors/NotificationLoggingBehavior.cs | 6 ++++++ .../Behaviors/RequestProcessorBehavior.cs | 6 ++++++ .../ServiceCollectionExtensions.cs | 6 ++++++ src/KSFramework/Messaging/Mediator.cs | 6 ++++++ .../Messaging/NotificationLoggingBehavior.cs | 6 ++++++ .../Samples/UserRegisteredHandler.cs | 6 ++++++ .../Samples/UserRegisteredNotification.cs | 6 ++++++ 27 files changed, 189 insertions(+) create mode 100644 Samples/MediatorSampleApp/CounterStreamTest.cs create mode 100644 Samples/MediatorSampleApp/LoggingNotificationBehavior.cs create mode 100644 Samples/MediatorSampleApp/MultiplyByTwoRequest.cs create mode 100644 Samples/MediatorSampleApp/NotificationTest.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IMediator.cs create mode 100644 src/KSFramework/Messaging/Abstraction/INotification.cs create mode 100644 src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs create mode 100644 src/KSFramework/Messaging/Abstraction/INotificationHandler.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IPostProcessor.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IRequest.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IRequestHandler.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs create mode 100644 src/KSFramework/Messaging/Abstraction/ISender.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IStreamRequest.cs create mode 100644 src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs create mode 100644 src/KSFramework/Messaging/Abstraction/Unit.cs create mode 100644 src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs create mode 100644 src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs create mode 100644 src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs create mode 100644 src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs create mode 100644 src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs create mode 100644 src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs create mode 100644 src/KSFramework/Messaging/Mediator.cs create mode 100644 src/KSFramework/Messaging/NotificationLoggingBehavior.cs create mode 100644 src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs create mode 100644 src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs diff --git a/Samples/MediatorSampleApp/CounterStreamTest.cs b/Samples/MediatorSampleApp/CounterStreamTest.cs new file mode 100644 index 0000000..70957da --- /dev/null +++ b/Samples/MediatorSampleApp/CounterStreamTest.cs @@ -0,0 +1,6 @@ +namespace Mediator; + +public class CounterStreamTest +{ + +} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs b/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs new file mode 100644 index 0000000..0f2bfb3 --- /dev/null +++ b/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs @@ -0,0 +1,6 @@ +namespace Mediator; + +public class LoggingNotificationBehavior +{ + +} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs b/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs new file mode 100644 index 0000000..6789fec --- /dev/null +++ b/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs @@ -0,0 +1,6 @@ +namespace Mediator; + +public class MultiplyByTwoRequest +{ + +} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/NotificationTest.cs b/Samples/MediatorSampleApp/NotificationTest.cs new file mode 100644 index 0000000..4a078a4 --- /dev/null +++ b/Samples/MediatorSampleApp/NotificationTest.cs @@ -0,0 +1,6 @@ +namespace Mediator; + +public class NotificationTest +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IMediator.cs b/src/KSFramework/Messaging/Abstraction/IMediator.cs new file mode 100644 index 0000000..1ae3872 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IMediator.cs @@ -0,0 +1,19 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging; + +/// +/// Represents a mediator that supports sending requests and publishing notifications. +/// +public interface IMediator : ISender +{ + /// + /// Publishes a notification to all registered handlers. + /// + /// The notification type. + /// The notification instance. + /// Cancellation token. + /// A task representing the asynchronous operation. + Task Publish(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification; +} diff --git a/src/KSFramework/Messaging/Abstraction/INotification.cs b/src/KSFramework/Messaging/Abstraction/INotification.cs new file mode 100644 index 0000000..1f3207b --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/INotification.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface INotification +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs b/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs new file mode 100644 index 0000000..40bf611 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface INotificationBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs b/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs new file mode 100644 index 0000000..d71d634 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface INotificationHandler +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs b/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs new file mode 100644 index 0000000..678f973 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface IPipelineBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs b/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs new file mode 100644 index 0000000..6efee3b --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface IPostProcessor +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IRequest.cs b/src/KSFramework/Messaging/Abstraction/IRequest.cs new file mode 100644 index 0000000..5ae1e10 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IRequest.cs @@ -0,0 +1,5 @@ +namespace KSFramework.Messaging; + +public interface IRequest +{ +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs b/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs new file mode 100644 index 0000000..875b289 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs @@ -0,0 +1,7 @@ +namespace KSFramework.Messaging; + +public interface IRequestHandler + where TRequest : IRequest +{ + Task Handle(TRequest request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs b/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs new file mode 100644 index 0000000..32a33bf --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface IRequestPreProcessor +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/ISender.cs b/src/KSFramework/Messaging/Abstraction/ISender.cs new file mode 100644 index 0000000..9ed694e --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/ISender.cs @@ -0,0 +1,16 @@ +namespace KSFramework.Messaging; + +/// +/// Represents a sender that can send requests and return responses. +/// +public interface ISender +{ + /// + /// Sends a request asynchronously and returns the response. + /// + /// The type of the response. + /// The request to send. + /// Cancellation token. + /// The response from the request handler. + Task Send(IRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs b/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs new file mode 100644 index 0000000..0379746 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface IStreamRequest +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs b/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs new file mode 100644 index 0000000..1dd6d07 --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public interface IStreamRequestHandler +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/Unit.cs b/src/KSFramework/Messaging/Abstraction/Unit.cs new file mode 100644 index 0000000..d0904bb --- /dev/null +++ b/src/KSFramework/Messaging/Abstraction/Unit.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Abstraction; + +public struct Unit +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs b/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000..1bf0a33 --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Behaviors; + +public class LoggingBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs b/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs new file mode 100644 index 0000000..45a019f --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Behaviors; + +public class LoggingPostProcessor +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs b/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs new file mode 100644 index 0000000..5a22127 --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs @@ -0,0 +1,10 @@ +using KSFramework.Messaging.Abstraction; + +public class LoggingPreProcessor : IRequestPreProcessor +{ + public Task Process(TRequest request, CancellationToken cancellationToken) + { + Console.WriteLine($"[PreProcessor] Handling request of type: {typeof(TRequest).Name}"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs b/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs new file mode 100644 index 0000000..3ac3e70 --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Behaviors; + +public class NotificationLoggingBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs b/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs new file mode 100644 index 0000000..330a1aa --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Behaviors; + +public class RequestProcessorBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs b/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..fa75952 --- /dev/null +++ b/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Configuration; + +public class ServiceCollectionExtensions +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Mediator.cs b/src/KSFramework/Messaging/Mediator.cs new file mode 100644 index 0000000..4f5157d --- /dev/null +++ b/src/KSFramework/Messaging/Mediator.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging; + +public class Mediator +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/NotificationLoggingBehavior.cs b/src/KSFramework/Messaging/NotificationLoggingBehavior.cs new file mode 100644 index 0000000..3417a5f --- /dev/null +++ b/src/KSFramework/Messaging/NotificationLoggingBehavior.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging; + +public class NotificationLoggingBehavior +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs b/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs new file mode 100644 index 0000000..3291ec3 --- /dev/null +++ b/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Sample; + +public class UserRegisteredHandler +{ + +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs b/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs new file mode 100644 index 0000000..8ce56ee --- /dev/null +++ b/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs @@ -0,0 +1,6 @@ +namespace KSFramework.Messaging.Samples; + +public class UserRegisteredNotification +{ + +} \ No newline at end of file From 3f831d7490d9adc73890a2d76712ffbd236be0d6 Mon Sep 17 00:00:00 2001 From: Kamran Sadin Date: Tue, 3 Jun 2025 07:15:32 +0330 Subject: [PATCH 4/4] Add MediatR feature and some tests and Sample Console App --- KSFramework.sln | 40 ++++ .../MediatorSampleApp/CounterStreamTest.cs | 6 - .../LoggingNotificationBehavior.cs | 6 - Samples/MediatorSampleApp/Mediator.csproj | 14 ++ .../MediatorSampleApp/MultiplyByTwoRequest.cs | 6 - Samples/MediatorSampleApp/NotificationTest.cs | 6 - Samples/MediatorSampleApp/Program.cs | 29 +++ src/KSFramework/Domain/IDomainEvent.cs | 2 +- src/KSFramework/KSFramework.csproj | 7 +- .../Messaging/Abstraction/IMediator.cs | 31 ++- .../Messaging/Abstraction/INotification.cs | 4 +- .../Abstraction/INotificationBehavior.cs | 13 +- .../Abstraction/INotificationHandler.cs | 14 +- .../Abstraction/IPipelineBehavior.cs | 29 ++- .../Messaging/Abstraction/IPostProcessor.cs | 7 +- .../Messaging/Abstraction/IRequest.cs | 6 +- .../Messaging/Abstraction/IRequestHandler.cs | 9 +- .../Abstraction/IRequestPreProcessor.cs | 7 +- .../Messaging/Abstraction/ISender.cs | 2 +- .../Messaging/Abstraction/IStreamRequest.cs | 9 +- .../Abstraction/IStreamRequestHandler.cs | 10 +- src/KSFramework/Messaging/Abstraction/Unit.cs | 6 +- .../Behaviors/ExceptionHandlingBehavior.cs | 32 +++ .../Messaging/Behaviors/LoggingBehavior.cs | 28 ++- .../Behaviors/LoggingPostProcessor.cs | 10 +- .../Behaviors/LoggingPreProcessor.cs | 2 + .../Behaviors/NotificationLoggingBehavior.cs | 22 +- .../Behaviors/RequestProcessorBehavior.cs | 27 ++- .../Behaviors/RequestValidationBehavior.cs | 56 +++++ .../ServiceCollectionExtensions.cs | 50 +++- src/KSFramework/Messaging/Mediator.cs | 216 +++++++++++++++++- .../Messaging/NotificationLoggingBehavior.cs | 20 +- .../Messaging/Samples/CounterStreamTest.cs | 25 ++ .../Samples/LoggingNotificationBehavior.cs | 14 ++ .../Messaging/Samples/MultiplyByTwoRequest.cs | 18 ++ .../Messaging/Samples/NotificationTest.cs | 17 ++ .../Samples/UserRegisteredHandler.cs | 22 +- .../Samples/UserRegisteredNotification.cs | 14 +- .../Configuration/AddMessagingTests.cs | 27 +++ .../ExceptionHandlingBehaviorTests.cs | 42 ++++ .../KSFramework.UnitTests.csproj | 27 +++ .../RequestValidationBehaviorTests.cs | 181 +++++++++++++++ 42 files changed, 1037 insertions(+), 76 deletions(-) delete mode 100644 Samples/MediatorSampleApp/CounterStreamTest.cs delete mode 100644 Samples/MediatorSampleApp/LoggingNotificationBehavior.cs create mode 100644 Samples/MediatorSampleApp/Mediator.csproj delete mode 100644 Samples/MediatorSampleApp/MultiplyByTwoRequest.cs delete mode 100644 Samples/MediatorSampleApp/NotificationTest.cs create mode 100644 Samples/MediatorSampleApp/Program.cs create mode 100644 src/KSFramework/Messaging/Behaviors/ExceptionHandlingBehavior.cs create mode 100644 src/KSFramework/Messaging/Behaviors/RequestValidationBehavior.cs create mode 100644 src/KSFramework/Messaging/Samples/CounterStreamTest.cs create mode 100644 src/KSFramework/Messaging/Samples/LoggingNotificationBehavior.cs create mode 100644 src/KSFramework/Messaging/Samples/MultiplyByTwoRequest.cs create mode 100644 src/KSFramework/Messaging/Samples/NotificationTest.cs create mode 100644 tests/KSFramework/KSFramework.UnitTests/Configuration/AddMessagingTests.cs create mode 100644 tests/KSFramework/KSFramework.UnitTests/ExceptionHandlingBehaviorTests.cs create mode 100644 tests/KSFramework/KSFramework.UnitTests/KSFramework.UnitTests.csproj create mode 100644 tests/KSFramework/KSFramework.UnitTests/RequestValidationBehaviorTests.cs diff --git a/KSFramework.sln b/KSFramework.sln index 37d31fa..4a7b249 100644 --- a/KSFramework.sln +++ b/KSFramework.sln @@ -7,6 +7,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSFramework", "src\KSFramework\KSFramework.csproj", "{B335BD79-05C0-4F0E-A2DA-FC72DF07A18C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MediatorSampleApp", "MediatorSampleApp", "{B36C9166-6687-1700-0084-D88BD073518A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator", "Samples\MediatorSampleApp\Mediator.csproj", "{D849CD75-73AE-4F1A-80EA-0FC596C80723}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "KSFramework", "KSFramework", "{6F746C7E-99E8-D040-B792-CDA2514E83CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSFramework.UnitTests", "tests\KSFramework\KSFramework.UnitTests\KSFramework.UnitTests.csproj", "{48D1B7B2-D99B-4554-BBFB-95E75720B8E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,11 +41,39 @@ Global {B335BD79-05C0-4F0E-A2DA-FC72DF07A18C}.Release|x64.Build.0 = Release|Any CPU {B335BD79-05C0-4F0E-A2DA-FC72DF07A18C}.Release|x86.ActiveCfg = Release|Any CPU {B335BD79-05C0-4F0E-A2DA-FC72DF07A18C}.Release|x86.Build.0 = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|x64.ActiveCfg = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|x64.Build.0 = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|x86.ActiveCfg = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Debug|x86.Build.0 = Debug|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|Any CPU.Build.0 = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|x64.ActiveCfg = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|x64.Build.0 = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|x86.ActiveCfg = Release|Any CPU + {D849CD75-73AE-4F1A-80EA-0FC596C80723}.Release|x86.Build.0 = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|x64.Build.0 = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Debug|x86.Build.0 = Debug|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|Any CPU.Build.0 = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|x64.ActiveCfg = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|x64.Build.0 = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|x86.ActiveCfg = Release|Any CPU + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {B335BD79-05C0-4F0E-A2DA-FC72DF07A18C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {B36C9166-6687-1700-0084-D88BD073518A} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {D849CD75-73AE-4F1A-80EA-0FC596C80723} = {B36C9166-6687-1700-0084-D88BD073518A} + {6F746C7E-99E8-D040-B792-CDA2514E83CE} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {48D1B7B2-D99B-4554-BBFB-95E75720B8E5} = {6F746C7E-99E8-D040-B792-CDA2514E83CE} EndGlobalSection EndGlobal diff --git a/Samples/MediatorSampleApp/CounterStreamTest.cs b/Samples/MediatorSampleApp/CounterStreamTest.cs deleted file mode 100644 index 70957da..0000000 --- a/Samples/MediatorSampleApp/CounterStreamTest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Mediator; - -public class CounterStreamTest -{ - -} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs b/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs deleted file mode 100644 index 0f2bfb3..0000000 --- a/Samples/MediatorSampleApp/LoggingNotificationBehavior.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Mediator; - -public class LoggingNotificationBehavior -{ - -} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/Mediator.csproj b/Samples/MediatorSampleApp/Mediator.csproj new file mode 100644 index 0000000..6a2e3de --- /dev/null +++ b/Samples/MediatorSampleApp/Mediator.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs b/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs deleted file mode 100644 index 6789fec..0000000 --- a/Samples/MediatorSampleApp/MultiplyByTwoRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Mediator; - -public class MultiplyByTwoRequest -{ - -} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/NotificationTest.cs b/Samples/MediatorSampleApp/NotificationTest.cs deleted file mode 100644 index 4a078a4..0000000 --- a/Samples/MediatorSampleApp/NotificationTest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Mediator; - -public class NotificationTest -{ - -} \ No newline at end of file diff --git a/Samples/MediatorSampleApp/Program.cs b/Samples/MediatorSampleApp/Program.cs new file mode 100644 index 0000000..3857774 --- /dev/null +++ b/Samples/MediatorSampleApp/Program.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using KSFramework.Messaging; +using KSFramework.Messaging.Abstraction; +using KSFramework.Messaging.Behaviors; +using KSFramework.Messaging.Configuration; +using KSFramework.Messaging.Samples; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// فقط یکبار mediator رو ثبت کن +services.AddScoped(); +services.AddScoped(sp => sp.GetRequiredService()); + +services.AddLogging(); + +services.AddValidatorsFromAssembly(typeof(Program).Assembly); +services.AddMessaging(typeof(MultiplyByTwoHandler).Assembly); + +// اگر رفتارهای pipeline داری، ثبت کن +services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestProcessorBehavior<,>)); +services.AddMessaging(typeof(ExceptionHandlingBehavior<,>).Assembly, typeof(MultiplyByTwoRequest).Assembly); + +var provider = services.BuildServiceProvider(); + +var mediator = provider.GetRequiredService(); + +var result = await mediator.Send(new MultiplyByTwoRequest(5)); +Console.WriteLine($"Result: {result}"); \ No newline at end of file diff --git a/src/KSFramework/Domain/IDomainEvent.cs b/src/KSFramework/Domain/IDomainEvent.cs index 7562255..09c10b2 100644 --- a/src/KSFramework/Domain/IDomainEvent.cs +++ b/src/KSFramework/Domain/IDomainEvent.cs @@ -1,4 +1,4 @@ -using MediatR; +using KSFramework.Messaging.Abstraction; namespace KSFramework.Domain; public interface IDomainEvent : INotification diff --git a/src/KSFramework/KSFramework.csproj b/src/KSFramework/KSFramework.csproj index 97b1869..d3bf821 100644 --- a/src/KSFramework/KSFramework.csproj +++ b/src/KSFramework/KSFramework.csproj @@ -28,8 +28,10 @@ - - + + + + @@ -38,6 +40,7 @@ + diff --git a/src/KSFramework/Messaging/Abstraction/IMediator.cs b/src/KSFramework/Messaging/Abstraction/IMediator.cs index 1ae3872..1df2a1a 100644 --- a/src/KSFramework/Messaging/Abstraction/IMediator.cs +++ b/src/KSFramework/Messaging/Abstraction/IMediator.cs @@ -1,6 +1,4 @@ -using KSFramework.Messaging.Abstraction; - -namespace KSFramework.Messaging; +namespace KSFramework.Messaging.Abstraction; /// /// Represents a mediator that supports sending requests and publishing notifications. @@ -16,4 +14,31 @@ public interface IMediator : ISender /// A task representing the asynchronous operation. Task Publish(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification; + + /// + /// Sends a request without knowing the response type at compile time. + /// + /// The request to send. + /// Cancellation token. + /// A task that represents the response. + Task Send(object request, CancellationToken cancellationToken = default); + + /// + /// Publishes a notification without knowing the notification type at compile time. + /// + /// The notification instance. + /// Cancellation token. + /// A task representing the asynchronous operation. + Task Publish(object notification, CancellationToken cancellationToken = default); + + /// + /// Creates a stream of responses for a given stream request. + /// + /// Type of the streamed response. + /// The stream request instance. + /// Cancellation token. + /// An asynchronous stream of responses. + IAsyncEnumerable CreateStream( + IStreamRequest request, + CancellationToken cancellationToken = default); } diff --git a/src/KSFramework/Messaging/Abstraction/INotification.cs b/src/KSFramework/Messaging/Abstraction/INotification.cs index 1f3207b..d49298a 100644 --- a/src/KSFramework/Messaging/Abstraction/INotification.cs +++ b/src/KSFramework/Messaging/Abstraction/INotification.cs @@ -1,6 +1,8 @@ namespace KSFramework.Messaging.Abstraction; +/// +/// Marker interface to represent a notification message. +/// public interface INotification { - } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs b/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs index 40bf611..0fd7dd8 100644 --- a/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs +++ b/src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs @@ -1,6 +1,15 @@ namespace KSFramework.Messaging.Abstraction; -public interface INotificationBehavior +/// +/// Delegate that represents the next notification handler or behavior in the pipeline. +/// +public delegate Task NotificationHandlerDelegate(); + +/// +/// Interface for notification pipeline behaviors. +/// +/// Type of the notification. +public interface INotificationBehavior where TNotification : INotification { - + Task Handle(TNotification notification, CancellationToken cancellationToken, NotificationHandlerDelegate next); } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs b/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs index d71d634..ce192f6 100644 --- a/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs +++ b/src/KSFramework/Messaging/Abstraction/INotificationHandler.cs @@ -1,6 +1,14 @@ namespace KSFramework.Messaging.Abstraction; -public interface INotificationHandler +/// +/// Defines a handler for a specific type of notification. +/// +/// The notification type. +public interface INotificationHandler where TNotification : INotification { - -} \ No newline at end of file + /// + /// Handles the notification asynchronously. + /// + /// The notification instance. + Task Handle(TNotification notification, CancellationToken cancellationToken); +} diff --git a/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs b/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs index 678f973..6899e6f 100644 --- a/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs +++ b/src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs @@ -1,6 +1,27 @@ -namespace KSFramework.Messaging.Abstraction; +using System.Threading; +using System.Threading.Tasks; -public interface IPipelineBehavior +namespace KSFramework.Messaging.Abstraction; +/// +/// Defines a pipeline behavior for requests that can run code before and after the handler is invoked. +/// +/// Type of the request. +/// Type of the response. +public interface IPipelineBehavior { - -} \ No newline at end of file + /// + /// Executes behavior around the handler invocation. + /// + /// The request instance. + /// Cancellation token. + /// The next delegate in the pipeline, i.e., the handler or next behavior. + /// The response from the next delegate. + Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next); +} + +/// +/// Delegate representing the next step in the pipeline. +/// +/// The response type. +/// A task that completes with the response. +public delegate Task RequestHandlerDelegate(); \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs b/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs index 6efee3b..bcde205 100644 --- a/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs +++ b/src/KSFramework/Messaging/Abstraction/IPostProcessor.cs @@ -1,6 +1,9 @@ namespace KSFramework.Messaging.Abstraction; -public interface IPostProcessor +/// +/// Defines a post-processor that runs after the request handler. +/// +public interface IRequestPostProcessor { - + Task Process(TRequest request, TResponse response, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IRequest.cs b/src/KSFramework/Messaging/Abstraction/IRequest.cs index 5ae1e10..bff54c1 100644 --- a/src/KSFramework/Messaging/Abstraction/IRequest.cs +++ b/src/KSFramework/Messaging/Abstraction/IRequest.cs @@ -1,5 +1,7 @@ -namespace KSFramework.Messaging; +namespace KSFramework.Messaging.Abstraction; public interface IRequest { -} \ No newline at end of file +} + +public interface IRequest : IRequest { } diff --git a/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs b/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs index 875b289..bf13151 100644 --- a/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs +++ b/src/KSFramework/Messaging/Abstraction/IRequestHandler.cs @@ -1,7 +1,12 @@ -namespace KSFramework.Messaging; +namespace KSFramework.Messaging.Abstraction; public interface IRequestHandler where TRequest : IRequest { Task Handle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} + +public interface IRequestHandler : IRequestHandler + where TRequest : IRequest +{ +} diff --git a/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs b/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs index 32a33bf..f11aa7f 100644 --- a/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs +++ b/src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs @@ -1,6 +1,9 @@ namespace KSFramework.Messaging.Abstraction; -public interface IRequestPreProcessor +/// +/// Defines a pre-processor that runs before the request handler. +/// +public interface IRequestPreProcessor { - + Task Process(TRequest request, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/ISender.cs b/src/KSFramework/Messaging/Abstraction/ISender.cs index 9ed694e..35b2c7a 100644 --- a/src/KSFramework/Messaging/Abstraction/ISender.cs +++ b/src/KSFramework/Messaging/Abstraction/ISender.cs @@ -1,4 +1,4 @@ -namespace KSFramework.Messaging; +namespace KSFramework.Messaging.Abstraction; /// /// Represents a sender that can send requests and return responses. diff --git a/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs b/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs index 0379746..fb71bae 100644 --- a/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs +++ b/src/KSFramework/Messaging/Abstraction/IStreamRequest.cs @@ -1,6 +1,7 @@ namespace KSFramework.Messaging.Abstraction; -public interface IStreamRequest -{ - -} \ No newline at end of file +/// +/// Marker interface for a request that returns a stream of responses. +/// +/// Type of the streamed response. +public interface IStreamRequest : IRequest> { } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs b/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs index 1dd6d07..9f354ad 100644 --- a/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs +++ b/src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs @@ -1,6 +1,12 @@ namespace KSFramework.Messaging.Abstraction; -public interface IStreamRequestHandler +/// +/// Handles stream requests. +/// +/// The type of request. +/// The type of stream response. +public interface IStreamRequestHandler + where TRequest : IStreamRequest { - + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Abstraction/Unit.cs b/src/KSFramework/Messaging/Abstraction/Unit.cs index d0904bb..f8df51d 100644 --- a/src/KSFramework/Messaging/Abstraction/Unit.cs +++ b/src/KSFramework/Messaging/Abstraction/Unit.cs @@ -1,6 +1,6 @@ namespace KSFramework.Messaging.Abstraction; -public struct Unit +public readonly struct Unit { - -} \ No newline at end of file + public static readonly Unit Value = new(); +} diff --git a/src/KSFramework/Messaging/Behaviors/ExceptionHandlingBehavior.cs b/src/KSFramework/Messaging/Behaviors/ExceptionHandlingBehavior.cs new file mode 100644 index 0000000..ebe2639 --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/ExceptionHandlingBehavior.cs @@ -0,0 +1,32 @@ +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.Logging; + +namespace KSFramework.Messaging.Behaviors +{ + /// + /// Behavior that catches exceptions during request handling, logs them, and rethrows. + /// + public class ExceptionHandlingBehavior : IPipelineBehavior + where TRequest : IRequest + { + private readonly ILogger> _logger; + + public ExceptionHandlingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + try + { + return await next(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception caught in {Behavior} handling request {RequestType}", nameof(ExceptionHandlingBehavior), typeof(TRequest).Name); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs b/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs index 1bf0a33..6ebce8f 100644 --- a/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs +++ b/src/KSFramework/Messaging/Behaviors/LoggingBehavior.cs @@ -1,6 +1,30 @@ +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.Logging; + namespace KSFramework.Messaging.Behaviors; -public class LoggingBehavior +/// +/// Logs the request before and after it is handled. +/// +/// The type of request. +/// The type of response. +public class LoggingBehavior : IPipelineBehavior { - + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + _logger.LogInformation("[Logging] Handling {Request}", typeof(TRequest).Name); + var response = await next(); + _logger.LogInformation("[Logging] Handled {Request}", typeof(TRequest).Name); + return response; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs b/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs index 45a019f..452e6a4 100644 --- a/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs +++ b/src/KSFramework/Messaging/Behaviors/LoggingPostProcessor.cs @@ -1,6 +1,12 @@ +using KSFramework.Messaging.Abstraction; + namespace KSFramework.Messaging.Behaviors; -public class LoggingPostProcessor +public class LoggingPostProcessor : IRequestPostProcessor { - + public Task Process(TRequest request, TResponse response, CancellationToken cancellationToken) + { + Console.WriteLine($"[PostProcessor] Handled request of type: {typeof(TRequest).Name} with response: {response}"); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs b/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs index 5a22127..aa5b028 100644 --- a/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs +++ b/src/KSFramework/Messaging/Behaviors/LoggingPreProcessor.cs @@ -1,5 +1,7 @@ using KSFramework.Messaging.Abstraction; +namespace KSFramework.Messaging.Behaviors; + public class LoggingPreProcessor : IRequestPreProcessor { public Task Process(TRequest request, CancellationToken cancellationToken) diff --git a/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs b/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs index 3ac3e70..ab5d181 100644 --- a/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs +++ b/src/KSFramework/Messaging/Behaviors/NotificationLoggingBehavior.cs @@ -1,6 +1,24 @@ +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.Logging; + namespace KSFramework.Messaging.Behaviors; -public class NotificationLoggingBehavior +public class NotificationLoggingBehavior : INotificationBehavior + where TNotification : INotification { - + private readonly ILogger> _logger; + + public NotificationLoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TNotification notification, CancellationToken cancellationToken, NotificationHandlerDelegate next) + { + _logger.LogInformation("Handling notification of type {NotificationType}", typeof(TNotification).Name); + + await next(); + + _logger.LogInformation("Handled notification of type {NotificationType}", typeof(TNotification).Name); + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs b/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs index 330a1aa..3979892 100644 --- a/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs +++ b/src/KSFramework/Messaging/Behaviors/RequestProcessorBehavior.cs @@ -1,6 +1,29 @@ +using KSFramework.Messaging.Abstraction; + namespace KSFramework.Messaging.Behaviors; -public class RequestProcessorBehavior +/// +/// Executes pre-processors and post-processors for the request. +/// +public class RequestProcessorBehavior( + IEnumerable> preProcessors, + IEnumerable> postProcessors) + : IPipelineBehavior + where TRequest : IRequest { - + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + foreach (var preProcessor in preProcessors) + await preProcessor.Process(request, cancellationToken); + + var response = await next(); + + foreach (var postProcessor in postProcessors) + await postProcessor.Process(request, response, cancellationToken); + + return response; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Behaviors/RequestValidationBehavior.cs b/src/KSFramework/Messaging/Behaviors/RequestValidationBehavior.cs new file mode 100644 index 0000000..e066f1c --- /dev/null +++ b/src/KSFramework/Messaging/Behaviors/RequestValidationBehavior.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.Extensions.Logging; +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging.Behaviors +{ + /// + /// Pipeline behavior for validating requests using FluentValidation validators. + /// Throws ValidationException if validation fails. + /// + /// The type of the request. + /// The type of the response. + public class RequestValidationBehavior : IPipelineBehavior + where TRequest : IRequest // مطمئن شو این شرط در درخواستت وجود دارد + { + private readonly IEnumerable> Validators; + private readonly ILogger> Logger; + + public RequestValidationBehavior( + IEnumerable> validators, + ILogger> logger) + { + Validators = validators; + Logger = logger; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + if (Validators.Any()) + { + var validationResults = await Task.WhenAll( + Validators.Select(v => v.ValidateAsync(request, cancellationToken))); + + var failures = validationResults + .Where(r => r != null) + .SelectMany(r => r.Errors ?? Enumerable.Empty()) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + { + Logger.LogWarning("Validation failed for {RequestType}: {Failures}", typeof(TRequest).Name, failures); + throw new ValidationException(failures); + } + } + + // اگر ولیدیتور نبود یا اعتبارسنجی رد نشد، اجرای هندر ادامه پیدا کند + return await next(); + } + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs b/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs index fa75952..5af6b4a 100644 --- a/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs +++ b/src/KSFramework/Messaging/Configuration/ServiceCollectionExtensions.cs @@ -1,6 +1,52 @@ +using KSFramework.Messaging.Abstraction; +using KSFramework.Messaging.Behaviors; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + namespace KSFramework.Messaging.Configuration; -public class ServiceCollectionExtensions +/// +/// Extension methods to register messaging components into the service collection. +/// +public static class ServiceCollectionExtensions { - + /// + /// Adds messaging components (Mediator, Handlers, Behaviors, Processors) to the service collection. + /// + /// The service collection. + /// Assemblies to scan for handlers. + /// The updated service collection. + public static IServiceCollection AddMessaging(this IServiceCollection services, params Assembly[] assemblies) + { + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes.AssignableTo(typeof(IRequestHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(INotificationHandler<>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(IStreamRequestHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(IPipelineBehavior<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(IRequestPreProcessor<>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(IRequestPostProcessor<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + ); + + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ExceptionHandlingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestProcessorBehavior<,>)); + + return services; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Mediator.cs b/src/KSFramework/Messaging/Mediator.cs index 4f5157d..29ea8d7 100644 --- a/src/KSFramework/Messaging/Mediator.cs +++ b/src/KSFramework/Messaging/Mediator.cs @@ -1,6 +1,220 @@ +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.DependencyInjection; + namespace KSFramework.Messaging; -public class Mediator +/// +/// The default implementation of the mediator pattern. +/// +public class Mediator : IMediator { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider to resolve handlers and behaviors. + public Mediator(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + public async Task Send(IRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + var requestType = request.GetType(); + var responseType = typeof(TResponse); + + // Get the handler type + var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, responseType); + var handler = _serviceProvider.GetService(handlerType); + if (handler == null) + throw new InvalidOperationException($"Handler for '{requestType.Name}' not found."); + + // Get all pipeline behaviors for this request/response + var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, responseType); + var behaviors = _serviceProvider.GetServices(behaviorType).Cast().Reverse().ToList(); + + // Create the final handler delegate + RequestHandlerDelegate handlerDelegate = () => + { + var method = handlerType.GetMethod("Handle"); + return (Task)method.Invoke(handler, new object[] { request, cancellationToken }); + }; + + // Compose the pipeline by wrapping behaviors around the handler delegate + foreach (var behavior in behaviors) + { + var currentBehavior = behavior; + var next = handlerDelegate; + handlerDelegate = () => + { + var method = behaviorType.GetMethod("Handle"); + return (Task)method.Invoke(currentBehavior, new object[] { request, cancellationToken, next }); + }; + } + + // Execute the composed pipeline + return await handlerDelegate(); + } + + public async Task Send(object request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + var requestType = request.GetType(); + + var requestInterface = requestType + .GetInterfaces() + .FirstOrDefault(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IRequest<>)); + + if (requestInterface == null) + throw new InvalidOperationException("Request does not implement IRequest<>"); + + var responseType = requestInterface.GetGenericArguments()[0]; + + var method = typeof(Mediator) + .GetMethods() + .FirstOrDefault(m => + m.Name == nameof(Send) + && m.IsGenericMethodDefinition + && m.GetGenericArguments().Length == 1 + && m.GetParameters().Length == 2 + && m.GetParameters()[0].ParameterType.IsGenericType + && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IRequest<>)); + + if (method == null) + throw new InvalidOperationException("Unable to find the correct generic Send method."); + + var genericMethod = method.MakeGenericMethod(responseType); + + var task = (Task)genericMethod.Invoke(this, new object[] { request, cancellationToken })!; + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty?.GetValue(task); + } + /// + public async Task Publish(object notification, CancellationToken cancellationToken = default) + { + if (notification == null) + throw new ArgumentNullException(nameof(notification)); + + var notificationType = notification.GetType(); + + if (!typeof(INotification).IsAssignableFrom(notificationType)) + throw new InvalidOperationException($"'{notificationType.Name}' does not implement INotification."); + + var method = typeof(Mediator) + .GetMethods() + .FirstOrDefault(m => + m.Name == nameof(Publish) && + m.IsGenericMethodDefinition && + m.GetGenericArguments().Length == 1 && + m.GetParameters().Length == 2); + + if (method == null) + throw new InvalidOperationException("Unable to find generic Publish method."); + + var genericMethod = method.MakeGenericMethod(notificationType); + await (Task)genericMethod.Invoke(this, new object[] { notification, cancellationToken })!; + } + + // /// + // public async Task Send(object request, CancellationToken cancellationToken = default) + // { + // if (request == null) + // throw new ArgumentNullException(nameof(request)); + // + // var requestType = request.GetType(); + // + // // پیدا کردن اینترفیس IRequest که روی request اعمال شده + // var interfaceType = requestType + // .GetInterfaces() + // .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequest<>)); + // + // if (interfaceType == null) + // throw new InvalidOperationException($"Request type '{requestType.Name}' does not implement IRequest<> interface."); + // + // var responseType = interfaceType.GetGenericArguments()[0]; + // + // // ساختن متد جنریک Send(IRequest) + // var method = typeof(IMediator) + // .GetMethods() + // .FirstOrDefault(m => + // m.Name == nameof(Send) && + // m.IsGenericMethodDefinition && + // m.GetParameters().Length == 2 && + // m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IRequest<>)); + // + // if (method == null) + // throw new InvalidOperationException("Unable to find generic Send method."); + // + // var genericMethod = method.MakeGenericMethod(responseType); + // + // return await (Task)genericMethod.Invoke(this, new object[] { request, cancellationToken }); + // } + + + /// + /// Publishes a notification to all registered handlers. + /// + /// The notification type. + /// The notification instance. + /// Cancellation token. + /// A task representing the asynchronous operation. + public async Task Publish(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + { + var notificationType = typeof(TNotification); + + var behaviorType = typeof(INotificationBehavior<>).MakeGenericType(notificationType); + var behaviors = _serviceProvider.GetServices(behaviorType).Cast().Reverse().ToList(); + + var handlers = _serviceProvider.GetServices>().ToList(); + + if (!handlers.Any()) + return; + + NotificationHandlerDelegate handlerDelegate = () => + { + var tasks = handlers.Select(h => h.Handle(notification, cancellationToken)); + return Task.WhenAll(tasks); + }; + + foreach (var behavior in behaviors) + { + var currentBehavior = behavior; + var next = handlerDelegate; + handlerDelegate = () => + { + var method = behavior.GetType().GetMethod("Handle"); + return (Task)method.Invoke(currentBehavior, new object[] { notification, cancellationToken, next }); + }; + } + + await handlerDelegate(); + } + + public IAsyncEnumerable CreateStream(IStreamRequest request, CancellationToken cancellationToken = default) + { + var handlerType = typeof(IStreamRequestHandler<,>).MakeGenericType(request.GetType(), typeof(TResponse)); + var handler = _serviceProvider.GetService(handlerType); + + if (handler == null) + throw new InvalidOperationException($"No stream handler registered for {request.GetType().Name}"); + + var method = handlerType.GetMethod("Handle"); + + var result = (IAsyncEnumerable)method.Invoke(handler, new object[] { request, cancellationToken }); + + return result; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/NotificationLoggingBehavior.cs b/src/KSFramework/Messaging/NotificationLoggingBehavior.cs index 3417a5f..54c0838 100644 --- a/src/KSFramework/Messaging/NotificationLoggingBehavior.cs +++ b/src/KSFramework/Messaging/NotificationLoggingBehavior.cs @@ -1,6 +1,22 @@ +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.Logging; + namespace KSFramework.Messaging; -public class NotificationLoggingBehavior +public class NotificationLoggingBehavior : INotificationBehavior + where TNotification : INotification { - + private readonly ILogger> _logger; + + public NotificationLoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TNotification notification, CancellationToken cancellationToken, NotificationHandlerDelegate next) + { + _logger.LogInformation($"[NotificationLogging] Handling {typeof(TNotification).Name} started."); + await next(); + _logger.LogInformation($"[NotificationLogging] Handling {typeof(TNotification).Name} finished."); + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/CounterStreamTest.cs b/src/KSFramework/Messaging/Samples/CounterStreamTest.cs new file mode 100644 index 0000000..dfde75b --- /dev/null +++ b/src/KSFramework/Messaging/Samples/CounterStreamTest.cs @@ -0,0 +1,25 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging.Samples; + +public class CounterStreamRequest : IStreamRequest +{ + public int CountTo { get; } + + public CounterStreamRequest(int countTo) + { + CountTo = countTo; + } +} + +public class CounterStreamHandler : IStreamRequestHandler +{ + public async IAsyncEnumerable Handle(CounterStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + for (int i = 1; i <= request.CountTo; i++) + { + yield return i; + await Task.Delay(200, cancellationToken); // simulate delay + } + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/LoggingNotificationBehavior.cs b/src/KSFramework/Messaging/Samples/LoggingNotificationBehavior.cs new file mode 100644 index 0000000..70c4ec0 --- /dev/null +++ b/src/KSFramework/Messaging/Samples/LoggingNotificationBehavior.cs @@ -0,0 +1,14 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging.Samples; + +public class LoggingNotificationBehavior : INotificationBehavior where TNotification : INotification +{ + public Task Handle(TNotification notification, CancellationToken cancellationToken, NotificationHandlerDelegate next) + { + Console.WriteLine($"Before handling notification of type {typeof(TNotification).Name}"); + var task = next(); + Console.WriteLine($"After handling notification of type {typeof(TNotification).Name}"); + return task; + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/MultiplyByTwoRequest.cs b/src/KSFramework/Messaging/Samples/MultiplyByTwoRequest.cs new file mode 100644 index 0000000..9116a5a --- /dev/null +++ b/src/KSFramework/Messaging/Samples/MultiplyByTwoRequest.cs @@ -0,0 +1,18 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging.Samples; + +public class MultiplyByTwoRequest : IRequest +{ + public int Input { get; } + + public MultiplyByTwoRequest(int input) => Input = input; +} + +public class MultiplyByTwoHandler : IRequestHandler +{ + public Task Handle(MultiplyByTwoRequest request, CancellationToken cancellationToken) + { + return Task.FromResult(request.Input * 2); + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/NotificationTest.cs b/src/KSFramework/Messaging/Samples/NotificationTest.cs new file mode 100644 index 0000000..c84f2a3 --- /dev/null +++ b/src/KSFramework/Messaging/Samples/NotificationTest.cs @@ -0,0 +1,17 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.Messaging.Samples; + +public class TestNotification : INotification +{ + public string Message { get; set; } = default!; +} + +public class TestNotificationHandler : INotificationHandler +{ + public Task Handle(TestNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"Received notification: {notification.Message}"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs b/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs index 3291ec3..3bbd816 100644 --- a/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs +++ b/src/KSFramework/Messaging/Samples/UserRegisteredHandler.cs @@ -1,6 +1,22 @@ -namespace KSFramework.Messaging.Sample; +using KSFramework.Messaging.Abstraction; +using Microsoft.Extensions.Logging; -public class UserRegisteredHandler +namespace KSFramework.Messaging.Samples; + +public class SendWelcomeEmailHandler : INotificationHandler +{ + public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"[Email] Welcome email sent to {notification.Username}"); + return Task.CompletedTask; + } +} + +public class LogUserRegistrationHandler : INotificationHandler { - + public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"[Log] User '{notification.Username}' has registered."); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs b/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs index 8ce56ee..fcfba17 100644 --- a/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs +++ b/src/KSFramework/Messaging/Samples/UserRegisteredNotification.cs @@ -1,6 +1,16 @@ +using KSFramework.Messaging.Abstraction; + namespace KSFramework.Messaging.Samples; -public class UserRegisteredNotification +/// +/// Notification that is triggered when a user registers. +/// +public class UserRegisteredNotification : INotification { - + public string Username { get; } + + public UserRegisteredNotification(string username) + { + Username = username; + } } \ No newline at end of file diff --git a/tests/KSFramework/KSFramework.UnitTests/Configuration/AddMessagingTests.cs b/tests/KSFramework/KSFramework.UnitTests/Configuration/AddMessagingTests.cs new file mode 100644 index 0000000..f4372c7 --- /dev/null +++ b/tests/KSFramework/KSFramework.UnitTests/Configuration/AddMessagingTests.cs @@ -0,0 +1,27 @@ +using KSFramework.Messaging.Abstraction; +using KSFramework.Messaging.Configuration; +using KSFramework.Messaging.Samples; +using Microsoft.Extensions.DependencyInjection; + +namespace KSFramework.UnitTests.Configuration; + +public class AddMessagingTests +{ + [Fact] + public void Should_Register_All_Handlers_And_Behaviors_From_Assembly() + { + // Arrange + var services = new ServiceCollection(); + + // اینجا مهمه: باید اسمبلی‌ای رو بده که MultiplyByTwoHandler توشه + services.AddMessaging(typeof(MultiplyByTwoRequest).Assembly); + + var provider = services.BuildServiceProvider(); + var handler = provider.GetService>(); + + // Assert + Assert.NotNull(handler); + var result = handler.Handle(new MultiplyByTwoRequest(5), CancellationToken.None).Result; + Assert.Equal(10, result); + } +} \ No newline at end of file diff --git a/tests/KSFramework/KSFramework.UnitTests/ExceptionHandlingBehaviorTests.cs b/tests/KSFramework/KSFramework.UnitTests/ExceptionHandlingBehaviorTests.cs new file mode 100644 index 0000000..7475f5d --- /dev/null +++ b/tests/KSFramework/KSFramework.UnitTests/ExceptionHandlingBehaviorTests.cs @@ -0,0 +1,42 @@ +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.UnitTests; + +using KSFramework.Messaging.Behaviors; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +public class ExceptionHandlingBehaviorTests +{ + public class TestRequest : IRequest { } + + [Fact] + public async Task Handle_WhenHandlerThrowsException_ShouldLogAndRethrow() + { + // Arrange + var loggerMock = new Mock>>(); + var behavior = new ExceptionHandlingBehavior(loggerMock.Object); + + var request = new TestRequest(); + + // next delegate that throws exception + RequestHandlerDelegate next = () => throw new InvalidOperationException("Test exception"); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => behavior.Handle(request, CancellationToken.None, next)); + + // Verify that logger.LogError was called with the exception + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception caught")), + ex, + It.IsAny>()), + Times.Once); + } +} \ No newline at end of file diff --git a/tests/KSFramework/KSFramework.UnitTests/KSFramework.UnitTests.csproj b/tests/KSFramework/KSFramework.UnitTests/KSFramework.UnitTests.csproj new file mode 100644 index 0000000..0d84793 --- /dev/null +++ b/tests/KSFramework/KSFramework.UnitTests/KSFramework.UnitTests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/tests/KSFramework/KSFramework.UnitTests/RequestValidationBehaviorTests.cs b/tests/KSFramework/KSFramework.UnitTests/RequestValidationBehaviorTests.cs new file mode 100644 index 0000000..cdf7810 --- /dev/null +++ b/tests/KSFramework/KSFramework.UnitTests/RequestValidationBehaviorTests.cs @@ -0,0 +1,181 @@ +using FluentValidation; +using FluentValidation.Results; +using Microsoft.Extensions.Logging; +using Moq; +using KSFramework.Messaging.Behaviors; +using KSFramework.Messaging.Abstraction; + +namespace KSFramework.UnitTests +{ + public class RequestValidationBehaviorTests + { + public class TestRequest : IRequest { } + + public class TestResponse + { + public string Message { get; set; } = "OK"; + } + + [Fact] + public async Task Handle_WithNoValidators_CallsNext() + { + var nextCalled = false; + Task Next() + { + nextCalled = true; + return Task.FromResult(new TestResponse()); + } + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + Array.Empty>(), + loggerMock.Object); + + var result = await behavior.Handle(new TestRequest(), CancellationToken.None, Next); + + Assert.True(nextCalled); + Assert.Equal("OK", result.Message); + } + + [Fact] + public async Task Handle_WithValidRequest_CallsNext() + { + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ValidationResult()); // no errors + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + new[] { validatorMock.Object }, + loggerMock.Object); + + var nextCalled = false; + Task Next() + { + nextCalled = true; + return Task.FromResult(new TestResponse()); + } + + var result = await behavior.Handle(new TestRequest(), CancellationToken.None, Next); + + Assert.True(nextCalled); + Assert.Equal("OK", result.Message); + } + + [Fact] + public async Task Handle_WithInvalidRequest_ThrowsValidationException() + { + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ValidationResult(new[] + { + new ValidationFailure("Prop", "Error") + })); + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + new[] { validatorMock.Object }, + loggerMock.Object); + + await Assert.ThrowsAsync(() => + behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse()))); + } + + [Fact] + public async Task Handle_WithInvalidRequest_ThrowsValidationException_ContainsErrorMessage() + { + var invalidFailures = new[] + { + new ValidationFailure("Name", "Name is required") + }; + + var validationResult = new ValidationResult(invalidFailures); + + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validationResult); + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + new[] { validatorMock.Object }, + loggerMock.Object); + + var ex = await Assert.ThrowsAsync(() => + behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse()))); + + Assert.Contains("Name is required", ex.Message); + } + + [Fact] + public async Task Handle_WithInvalidRequest_LogsWarning() + { + // Arrange + var invalidFailures = new List + { + new ValidationFailure("Name", "Name is required") + }; + + var validationResult = new ValidationResult(invalidFailures); + + var validatorMock = new Mock>(); + validatorMock.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validationResult); + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + new[] { validatorMock.Object }, + loggerMock.Object); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => + behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse()))); + + Assert.Contains("Name is required", ex.Message); + + // Verify that a Warning log was written containing the validation error message + loggerMock.Verify( + x => x.Log( + It.Is(l => l == LogLevel.Warning), + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Name is required")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task Handle_WithMultipleValidators_AggregatesErrors() + { + var validator1 = new Mock>(); + validator1.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ValidationResult(new[] + { + new ValidationFailure("Name", "Name is required") + })); + + var validator2 = new Mock>(); + validator2.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ValidationResult(new[] + { + new ValidationFailure("Email", "Email is invalid") + })); + + var loggerMock = new Mock>>(); + + var behavior = new RequestValidationBehavior( + new[] { validator1.Object, validator2.Object }, + loggerMock.Object); + + var ex = await Assert.ThrowsAsync(() => + behavior.Handle(new TestRequest(), CancellationToken.None, () => Task.FromResult(new TestResponse()))); + + Assert.Contains("Name is required", ex.Message); + Assert.Contains("Email is invalid", ex.Message); + } + } +} \ No newline at end of file