Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 40 additions & 0 deletions KSFramework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
14 changes: 14 additions & 0 deletions Samples/MediatorSampleApp/Mediator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\KSFramework\KSFramework.csproj" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions Samples/MediatorSampleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -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<IMediator, Mediator>();
services.AddScoped<ISender>(sp => sp.GetRequiredService<IMediator>());

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<IMediator>();

var result = await mediator.Send(new MultiplyByTwoRequest(5));
Console.WriteLine($"Result: {result}");
2 changes: 1 addition & 1 deletion src/KSFramework/Domain/IDomainEvent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using MediatR;
using KSFramework.Messaging.Abstraction;

namespace KSFramework.Domain;
public interface IDomainEvent : INotification
Expand Down
7 changes: 5 additions & 2 deletions src/KSFramework/KSFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<!-- TODO: To be removed-->
<!-- <PackageReference Include="MediatR" Version="12.5.0" />-->
<PackageReference Include="FluentValidation" Version="12.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
Expand All @@ -38,6 +40,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Pluralize.NET" Version="1.0.2" />
<PackageReference Include="Scrutor" Version="6.0.1" />
</ItemGroup>

</Project>
44 changes: 44 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IMediator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Represents a mediator that supports sending requests and publishing notifications.
/// </summary>
public interface IMediator : ISender
{
/// <summary>
/// Publishes a notification to all registered handlers.
/// </summary>
/// <typeparam name="TNotification">The notification type.</typeparam>
/// <param name="notification">The notification instance.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification;

/// <summary>
/// Sends a request without knowing the response type at compile time.
/// </summary>
/// <param name="request">The request to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the response.</returns>
Task<object?> Send(object request, CancellationToken cancellationToken = default);

/// <summary>
/// Publishes a notification without knowing the notification type at compile time.
/// </summary>
/// <param name="notification">The notification instance.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task Publish(object notification, CancellationToken cancellationToken = default);

/// <summary>
/// Creates a stream of responses for a given stream request.
/// </summary>
/// <typeparam name="TResponse">Type of the streamed response.</typeparam>
/// <param name="request">The stream request instance.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An asynchronous stream of responses.</returns>
IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default);
}
8 changes: 8 additions & 0 deletions src/KSFramework/Messaging/Abstraction/INotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Marker interface to represent a notification message.
/// </summary>
public interface INotification
{
}
15 changes: 15 additions & 0 deletions src/KSFramework/Messaging/Abstraction/INotificationBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Delegate that represents the next notification handler or behavior in the pipeline.
/// </summary>
public delegate Task NotificationHandlerDelegate();

/// <summary>
/// Interface for notification pipeline behaviors.
/// </summary>
/// <typeparam name="TNotification">Type of the notification.</typeparam>
public interface INotificationBehavior<TNotification> where TNotification : INotification
{
Task Handle(TNotification notification, CancellationToken cancellationToken, NotificationHandlerDelegate next);
}
14 changes: 14 additions & 0 deletions src/KSFramework/Messaging/Abstraction/INotificationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Defines a handler for a specific type of notification.
/// </summary>
/// <typeparam name="TNotification">The notification type.</typeparam>
public interface INotificationHandler<TNotification> where TNotification : INotification
{
/// <summary>
/// Handles the notification asynchronously.
/// </summary>
/// <param name="notification">The notification instance.</param>
Task Handle(TNotification notification, CancellationToken cancellationToken);
}
27 changes: 27 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IPipelineBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;

namespace KSFramework.Messaging.Abstraction;
/// <summary>
/// Defines a pipeline behavior for requests that can run code before and after the handler is invoked.
/// </summary>
/// <typeparam name="TRequest">Type of the request.</typeparam>
/// <typeparam name="TResponse">Type of the response.</typeparam>
public interface IPipelineBehavior<TRequest, TResponse>
{
/// <summary>
/// Executes behavior around the handler invocation.
/// </summary>
/// <param name="request">The request instance.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="next">The next delegate in the pipeline, i.e., the handler or next behavior.</param>
/// <returns>The response from the next delegate.</returns>
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}

/// <summary>
/// Delegate representing the next step in the pipeline.
/// </summary>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <returns>A task that completes with the response.</returns>
public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
9 changes: 9 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Defines a post-processor that runs after the request handler.
/// </summary>
public interface IRequestPostProcessor<in TRequest, in TResponse>
{
Task Process(TRequest request, TResponse response, CancellationToken cancellationToken);
}
7 changes: 7 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace KSFramework.Messaging.Abstraction;

public interface IRequest<TResponse>
{
}

public interface IRequest : IRequest<Unit> { }
12 changes: 12 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace KSFramework.Messaging.Abstraction;

public interface IRequestHandler<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

public interface IRequestHandler<TRequest> : IRequestHandler<TRequest, Unit>
where TRequest : IRequest
{
}
9 changes: 9 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IRequestPreProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Defines a pre-processor that runs before the request handler.
/// </summary>
public interface IRequestPreProcessor<in TRequest>
{
Task Process(TRequest request, CancellationToken cancellationToken);
}
16 changes: 16 additions & 0 deletions src/KSFramework/Messaging/Abstraction/ISender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Represents a sender that can send requests and return responses.
/// </summary>
public interface ISender
{
/// <summary>
/// Sends a request asynchronously and returns the response.
/// </summary>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <param name="request">The request to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The response from the request handler.</returns>
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
}
7 changes: 7 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IStreamRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Marker interface for a request that returns a stream of responses.
/// </summary>
/// <typeparam name="TResponse">Type of the streamed response.</typeparam>
public interface IStreamRequest<TResponse> : IRequest<IAsyncEnumerable<TResponse>> { }
12 changes: 12 additions & 0 deletions src/KSFramework/Messaging/Abstraction/IStreamRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace KSFramework.Messaging.Abstraction;

/// <summary>
/// Handles stream requests.
/// </summary>
/// <typeparam name="TRequest">The type of request.</typeparam>
/// <typeparam name="TResponse">The type of stream response.</typeparam>
public interface IStreamRequestHandler<in TRequest, TResponse>
where TRequest : IStreamRequest<TResponse>
{
IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
6 changes: 6 additions & 0 deletions src/KSFramework/Messaging/Abstraction/Unit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace KSFramework.Messaging.Abstraction;

public readonly struct Unit
{
public static readonly Unit Value = new();
}
32 changes: 32 additions & 0 deletions src/KSFramework/Messaging/Behaviors/ExceptionHandlingBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using KSFramework.Messaging.Abstraction;
using Microsoft.Extensions.Logging;

namespace KSFramework.Messaging.Behaviors
{
/// <summary>
/// Behavior that catches exceptions during request handling, logs them, and rethrows.
/// </summary>
public class ExceptionHandlingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> _logger;

public ExceptionHandlingBehavior(ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}

public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
try
{
return await next();
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception caught in {Behavior} handling request {RequestType}", nameof(ExceptionHandlingBehavior<TRequest, TResponse>), typeof(TRequest).Name);
throw;
}
}
}
}
Loading