Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ indent_size = 4
generated_code = true

# XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,nativeproj,locproj}]
[*.{slnx,csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,nativeproj,locproj}]
indent_size = 2
max_line_length = 160

# Xml build files
[*.builds]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ jobs:
with:
dotnetLogging: ${{ inputs.dotnet-logging }}
dotnetVersion: ${{ vars.NE_DOTNET_TARGETFRAMEWORKS }}
solution: ###SOLUTION###
solution: ./Pulse.slnx
secrets: inherit
14 changes: 7 additions & 7 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project>
<PropertyGroup Label="Package settings">
<Title>$(MSBuildProjectName)</Title>
<Description></Description>
<RepositoryUrl></RepositoryUrl>
<PackageProjectUrl></PackageProjectUrl>
<RepositoryUrl>https://github.com/dailydevops/pulse.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/dailydevops/pulse</PackageProjectUrl>
<PackageReleaseNotes>$(PackageProjectUrl)/releases</PackageReleaseNotes>
<PackageTags></PackageTags>
<CopyrightYearStart>2024</CopyrightYearStart>
<PackageTags>$(PackageTags);cqrs;mediator;</PackageTags>
<CopyrightYearStart>2026</CopyrightYearStart>
</PropertyGroup>

<PropertyGroup>
<!-- Workaround, until https://github.com/GitTools/GitVersion/pull/4206 is released -->
<GitVersionTargetFramework>net8.0</GitVersionTargetFramework>
<_ProjectTargetFrameworks>net8.0;net9.0;net10.0</_ProjectTargetFrameworks>
<_TestTargetFrameworks>net8.0;net9.0;net10.0</_TestTargetFrameworks>
</PropertyGroup>
</Project>
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@
<GlobalPackageReference Include="Roslynator.Refactorings" Version="4.15.0" />
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="10.18.0.131500" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="NetEvolve.Extensions.TUnit" Version="3.5.3" />
<PackageVersion Include="TUnit" Version="1.9.42" />
<PackageVersion Include="Verify.TUnit" Version="31.9.3" />
</ItemGroup>
</Project>
33 changes: 33 additions & 0 deletions Pulse.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path=".commitlintrc" />
<File Path=".csharpierignore" />
<File Path=".editorconfig" />
<File Path=".filenesting.json" />
<File Path=".gitattributes" />
<File Path=".gitignore" />
<File Path=".mcp.json" />
<File Path="AGENTS.md" />
<File Path="CODE_OF_CONDUCT.md" />
<File Path="CONTRIBUTING.md" />
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="Directory.Solution.props" />
<File Path="GitVersion.yml" />
<File Path="global.json" />
<File Path="LICENSE" />
<File Path="logo.png" />
<File Path="nuget.config" />
<File Path="README.md" />
<File Path="renovate.json" />
<File Path="testEnvironments.json" />
</Folder>
<Folder Name="/src/">
<Project Path="src/NetEvolve.Pulse.Extensibility/NetEvolve.Pulse.Extensibility.csproj" Id="3445330b-9648-432a-9333-e934ac0dd7dd" />
<Project Path="src/NetEvolve.Pulse/NetEvolve.Pulse.csproj" Id="0f9212e3-a314-47c5-842d-5702891b6d5a" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj" Id="99a08ce6-c633-4411-b2b2-edd5c5defeeb" />
<Project Path="tests/NetEvolve.Pulse.Tests.Unit/NetEvolve.Pulse.Tests.Unit.csproj" Id="cb20c880-dd1e-44c9-828e-cb3f208f1032" />
</Folder>
</Solution>
29 changes: 29 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/ICommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Represents a command that performs an action without returning a response value.
/// Commands are used to modify state or trigger actions in the system.
/// </summary>
/// <remarks>
/// This is a specialized version of <see cref="ICommand{TResponse}"/> that returns <see cref="Void"/>.
/// Use for operations like deletes or notifications that don't return data.
/// </remarks>
/// <example>
/// <code>
/// public record SendEmailCommand(string To, string Subject, string Body) : ICommand;
///
/// public class SendEmailCommandHandler : ICommandHandler&lt;SendEmailCommand, Void&gt;
/// {
/// private readonly IEmailService _emailService;
///
/// public async Task&lt;Void&gt; HandleAsync(SendEmailCommand command, CancellationToken cancellationToken)
/// {
/// await _emailService.SendAsync(command.To, command.Subject, command.Body, cancellationToken);
/// return Void.Completed;
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="ICommand{TResponse}"/>
/// <seealso cref="Void"/>
public interface ICommand : ICommand<Void>;
45 changes: 45 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/ICommandHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Defines a handler for processing commands of type <typeparamref name="TCommand"/> and producing responses of type <typeparamref name="TResponse"/>.
/// </summary>
/// <typeparam name="TCommand">The type of command to handle.</typeparam>
/// <typeparam name="TResponse">The type of response produced by the handler.</typeparam>
/// <remarks>
/// ⚠️ Each command type must have exactly one handler registered as a scoped service.
/// Handlers should manage their own transactions.
/// </remarks>
/// <example>
/// <code>
/// public record UpdateProductPriceCommand(string ProductId, decimal NewPrice)
/// : ICommand&lt;PriceUpdateResult&gt;;
///
/// public class UpdateProductPriceCommandHandler
/// : ICommandHandler&lt;UpdateProductPriceCommand, PriceUpdateResult&gt;
/// {
/// private readonly IProductRepository _repository;
///
/// public async Task&lt;PriceUpdateResult&gt; HandleAsync(
/// UpdateProductPriceCommand command, CancellationToken cancellationToken)
/// {
/// var product = await _repository.GetByIdAsync(command.ProductId, cancellationToken);
/// product.Price = command.NewPrice;
/// await _repository.UpdateAsync(product, cancellationToken);
/// return new PriceUpdateResult(product.Id, product.Price);
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="ICommand{TResponse}" />
/// <seealso cref="IMediator.SendAsync{TCommand, TResponse}" />
public interface ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
/// <summary>
/// Asynchronously handles the specified command and returns a response.
/// </summary>
/// <param name="command">The command to handle.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation, containing the command response.</returns>
Task<TResponse> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
16 changes: 16 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/ICommandInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Defines an interceptor for commands of type <typeparamref name="TCommand"/> that produce responses of type <typeparamref name="TResponse"/>.
/// Enables cross-cutting concerns like logging, validation, or transaction management for commands.
/// </summary>
/// <typeparam name="TCommand">The type of command to intercept.</typeparam>
/// <typeparam name="TResponse">The type of response produced by the command.</typeparam>
/// <remarks>
/// This interface extends <see cref="IRequestInterceptor{TRequest, TResponse}"/>.
/// See <see cref="IRequestInterceptor{TRequest, TResponse}"/> for implementation details.
/// </remarks>
/// <seealso cref="ICommand{TResponse}" />
/// <seealso cref="IRequestInterceptor{TRequest, TResponse}" />
public interface ICommandInterceptor<TCommand, TResponse> : IRequestInterceptor<TCommand, TResponse>
where TCommand : ICommand<TResponse>;
34 changes: 34 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/ICommand`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Represents a command that performs an action and returns a response of type <typeparamref name="TResponse"/>.
/// Commands are operations that change state or trigger side effects.
/// </summary>
/// <typeparam name="TResponse">The type of response returned after executing the command.</typeparam>
/// <remarks>
/// ⚠️ Each command type must have exactly one registered handler.
/// Use records for immutable command definitions.
/// </remarks>
/// <example>
/// <code>
/// public record CreateCustomerCommand(string Name, string Email) : ICommand&lt;CustomerCreatedResult&gt;;
/// public record CustomerCreatedResult(string CustomerId, DateTime CreatedAt);
///
/// public class CreateCustomerCommandHandler
/// : ICommandHandler&lt;CreateCustomerCommand, CustomerCreatedResult&gt;
/// {
/// private readonly ICustomerRepository _repository;
///
/// public async Task&lt;CustomerCreatedResult&gt; HandleAsync(
/// CreateCustomerCommand command, CancellationToken cancellationToken)
/// {
/// var customer = new Customer { Name = command.Name, Email = command.Email };
/// await _repository.AddAsync(customer, cancellationToken);
/// return new CustomerCreatedResult(customer.Id, customer.CreatedAt);
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="ICommand"/>
/// <seealso cref="ICommandHandler{TCommand, TResponse}"/>
public interface ICommand<TResponse> : IRequest<TResponse>;
51 changes: 51 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/IEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Represents an event that can be published through the mediator to notify multiple handlers.
/// Events are immutable notifications where multiple subscribers may react independently.
/// </summary>
/// <remarks>
/// ⚠️ Event handlers execute in parallel and should be idempotent. They should not depend on execution order.
/// Use past-tense names (OrderCreated, PaymentProcessed, UserRegistered).
/// </remarks>
/// <example>
/// <code>
/// public record OrderCreatedEvent : IEvent
/// {
/// public string Id { get; init; } = Guid.NewGuid().ToString();
/// public DateTimeOffset? PublishedAt { get; set; }
/// public string OrderId { get; init; }
/// public decimal TotalAmount { get; init; }
/// }
///
/// public class OrderCreatedEmailHandler : IEventHandler&lt;OrderCreatedEvent&gt;
/// {
/// private readonly IEmailService _emailService;
///
/// public async Task HandleAsync(OrderCreatedEvent message, CancellationToken cancellationToken)
/// {
/// await _emailService.SendOrderConfirmationAsync(message.OrderId, cancellationToken);
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="IEventHandler{TEvent}"/>
/// <seealso cref="IMediator.PublishAsync{TEvent}"/>
public interface IEvent
{
/// <summary>
/// Gets or sets the correlation identifier for tracing related operations.
/// </summary>
string? CorrelationId { get; set; }

/// <summary>
/// Gets the unique identifier for this event instance.
/// </summary>
string Id { get; }

/// <summary>
/// Gets or sets the timestamp when this event was published.
/// Automatically set by the mediator.
/// </summary>
DateTimeOffset? PublishedAt { get; set; }
}
45 changes: 45 additions & 0 deletions src/NetEvolve.Pulse.Extensibility/IEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace NetEvolve.Pulse.Extensibility;

/// <summary>
/// Defines a handler for processing events of type <typeparamref name="TEvent"/>.
/// Multiple handlers can be registered for the same event type and all execute in parallel.
/// </summary>
/// <typeparam name="TEvent">The type of event to handle.</typeparam>
/// <remarks>
/// ⚠️ Event handlers should be idempotent and handle failures gracefully.
/// Exceptions in one handler don't affect others.
/// </remarks>
/// <example>
/// <code>
/// public record UserRegisteredEvent : IEvent
/// {
/// public string Id { get; init; } = Guid.NewGuid().ToString();
/// public DateTimeOffset? PublishedAt { get; set; }
/// public string UserId { get; init; }
/// public string Email { get; init; }
/// }
///
/// public class UserRegisteredEmailHandler : IEventHandler&lt;UserRegisteredEvent&gt;
/// {
/// private readonly IEmailService _emailService;
///
/// public async Task HandleAsync(UserRegisteredEvent message, CancellationToken cancellationToken)
/// {
/// await _emailService.SendWelcomeEmailAsync(message.Email, cancellationToken);
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="IEvent" />
/// <seealso cref="IMediator.PublishAsync{TEvent}" />
public interface IEventHandler<in TEvent>
where TEvent : IEvent
{
/// <summary>
/// Asynchronously handles the specified event.
/// </summary>
/// <param name="message">The event to handle.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task HandleAsync(TEvent message, CancellationToken cancellationToken = default);
}
Loading