From 27338cdba46cad3397ed953be33bd65fc7ec1641 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Thu, 25 Sep 2025 10:55:57 +0300 Subject: [PATCH] feat: add domain event support --- FlowMediator.Console/Entities/TestEntity.cs | 18 ++++++++++ FlowMediator.Console/{ => Entities}/User.cs | 2 +- .../EventHandlers/TestEventHandler.cs | 15 ++++++++ FlowMediator.Console/Events/TestEvent.cs | 13 +++++++ FlowMediator.Console/Program.cs | 36 +++++++++++-------- .../{ => Queries}/GetUserByIdHandler.cs | 6 ++-- .../{ => Queries}/GetUserByIdQuery.cs | 5 +-- .../{ => Repositories}/IUserRepository.cs | 4 ++- .../Validators/GetUserByIdQueryValidator.cs | 3 +- FlowMediator/Contracts/BaseEntity.cs | 14 ++++++++ FlowMediator/Contracts/IDomainEvent.cs | 12 +++++++ FlowMediator/Contracts/Unit.cs | 11 ++++++ tests/FlowMediator.Tests/DomainEventTests.cs | 31 ++++++++++++++++ .../DomainEvents/TestEntity.cs | 17 +++++++++ .../DomainEvents/TestEvent.cs | 13 +++++++ .../DomainEvents/TestEventHandler.cs | 14 ++++++++ 16 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 FlowMediator.Console/Entities/TestEntity.cs rename FlowMediator.Console/{ => Entities}/User.cs (72%) create mode 100644 FlowMediator.Console/EventHandlers/TestEventHandler.cs create mode 100644 FlowMediator.Console/Events/TestEvent.cs rename FlowMediator.Console/{ => Queries}/GetUserByIdHandler.cs (78%) rename FlowMediator.Console/{ => Queries}/GetUserByIdQuery.cs (58%) rename FlowMediator.Console/{ => Repositories}/IUserRepository.cs (77%) create mode 100644 FlowMediator/Contracts/BaseEntity.cs create mode 100644 FlowMediator/Contracts/IDomainEvent.cs create mode 100644 FlowMediator/Contracts/Unit.cs create mode 100644 tests/FlowMediator.Tests/DomainEventTests.cs create mode 100644 tests/FlowMediator.Tests/DomainEvents/TestEntity.cs create mode 100644 tests/FlowMediator.Tests/DomainEvents/TestEvent.cs create mode 100644 tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs diff --git a/FlowMediator.Console/Entities/TestEntity.cs b/FlowMediator.Console/Entities/TestEntity.cs new file mode 100644 index 0000000..7059e40 --- /dev/null +++ b/FlowMediator.Console/Entities/TestEntity.cs @@ -0,0 +1,18 @@ +using FlowMediator.Console.Events; +using FlowMediator.Contracts; + +namespace FlowMediator.Console.Entities +{ + public class TestEntity : BaseEntity + { + public string Name { get; private set; } + + public static TestEntity Create(string name) + { + var entity = new TestEntity { Name = name }; + entity.AddDomainEvent(new TestEvent($"Entity {name} created")); + return entity; + } + } + +} diff --git a/FlowMediator.Console/User.cs b/FlowMediator.Console/Entities/User.cs similarity index 72% rename from FlowMediator.Console/User.cs rename to FlowMediator.Console/Entities/User.cs index 5988899..b4d7333 100644 --- a/FlowMediator.Console/User.cs +++ b/FlowMediator.Console/Entities/User.cs @@ -1,4 +1,4 @@ -namespace FlowMediator.Console +namespace FlowMediator.Console.Entities { public class User { diff --git a/FlowMediator.Console/EventHandlers/TestEventHandler.cs b/FlowMediator.Console/EventHandlers/TestEventHandler.cs new file mode 100644 index 0000000..8db22e9 --- /dev/null +++ b/FlowMediator.Console/EventHandlers/TestEventHandler.cs @@ -0,0 +1,15 @@ +using FlowMediator.Console.Events; +using FlowMediator.Contracts; + +namespace FlowMediator.Console.EventHandlers +{ + public class TestEventHandler : IRequestHandler + { + public Task Handle(TestEvent request, CancellationToken cancellationToken) + { + System.Console.WriteLine($"[Handler] Event triggered with message: {request.Message}"); + return Task.FromResult(Unit.Value); + } + } + +} diff --git a/FlowMediator.Console/Events/TestEvent.cs b/FlowMediator.Console/Events/TestEvent.cs new file mode 100644 index 0000000..944f612 --- /dev/null +++ b/FlowMediator.Console/Events/TestEvent.cs @@ -0,0 +1,13 @@ +using FlowMediator.Contracts; + +namespace FlowMediator.Console.Events +{ + public class TestEvent : IDomainEvent + { + public string Message { get; } + public DateTime OccurredOn { get; } = DateTime.UtcNow; + + public TestEvent(string message) => Message = message; + } + +} diff --git a/FlowMediator.Console/Program.cs b/FlowMediator.Console/Program.cs index 34f6984..429d0f6 100644 --- a/FlowMediator.Console/Program.cs +++ b/FlowMediator.Console/Program.cs @@ -1,5 +1,8 @@ using FlowMediator.Console; using FlowMediator.Console.Behaviors; +using FlowMediator.Console.Entities; +using FlowMediator.Console.Queries; +using FlowMediator.Console.Repositories; using FlowMediator.Contracts; using FlowMediator.Extensions; using FluentValidation; @@ -7,52 +10,55 @@ using System.Reflection; // handlers + behavior pipelines are automatic - //var services = new ServiceCollection(); - //services.AddFlowMediatorWithBehaviors(Assembly.GetExecutingAssembly()); - //services.AddSingleton(); - //services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - //var provider = services.BuildServiceProvider(); //var mediator = provider.GetRequiredService(); - // handlers are automatic, behavior pipelines manual var services = new ServiceCollection(); services.AddFlowMediator(Assembly.GetExecutingAssembly()); services.AddSingleton(); - services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); - - +// ------------------------ +// Query test +// ------------------------ var user = await mediator.SendAsync(new GetUserByIdQuery(1)); -Console.WriteLine($"User Id: {user.Id}, Name: {user.Name}"); - +System.Console.WriteLine($"User Id: {user.Id}, Name: {user.Name}"); try { var invalidUser = await mediator.SendAsync(new GetUserByIdQuery(0)); - Console.WriteLine($"User Id: {invalidUser.Id}, Name: {invalidUser.Name}"); + System.Console.WriteLine($"User Id: {invalidUser.Id}, Name: {invalidUser.Name}"); } catch (ValidationException ex) { - Console.WriteLine("Validation failed:"); + System.Console.WriteLine("Validation failed:"); foreach (var error in ex.Errors) { - Console.WriteLine($" - {error.PropertyName}: {error.ErrorMessage}"); + System.Console.WriteLine($" - {error.PropertyName}: {error.ErrorMessage}"); } } +// ------------------------ +// Domain Event test +// ------------------------ +var testEntity = TestEntity.Create("SampleEntity"); + + +foreach (var domainEvent in testEntity.DomainEvents) +{ + await mediator.SendAsync(domainEvent); +} +testEntity.ClearDomainEvents(); diff --git a/FlowMediator.Console/GetUserByIdHandler.cs b/FlowMediator.Console/Queries/GetUserByIdHandler.cs similarity index 78% rename from FlowMediator.Console/GetUserByIdHandler.cs rename to FlowMediator.Console/Queries/GetUserByIdHandler.cs index 2cc7ec9..4e8eeb5 100644 --- a/FlowMediator.Console/GetUserByIdHandler.cs +++ b/FlowMediator.Console/Queries/GetUserByIdHandler.cs @@ -1,6 +1,8 @@ -using FlowMediator.Contracts; +using FlowMediator.Console.Entities; +using FlowMediator.Console.Repositories; +using FlowMediator.Contracts; -namespace FlowMediator.Console +namespace FlowMediator.Console.Queries { public class GetUserByIdHandler : IRequestHandler { diff --git a/FlowMediator.Console/GetUserByIdQuery.cs b/FlowMediator.Console/Queries/GetUserByIdQuery.cs similarity index 58% rename from FlowMediator.Console/GetUserByIdQuery.cs rename to FlowMediator.Console/Queries/GetUserByIdQuery.cs index d387ff1..0c2eb56 100644 --- a/FlowMediator.Console/GetUserByIdQuery.cs +++ b/FlowMediator.Console/Queries/GetUserByIdQuery.cs @@ -1,6 +1,7 @@ -using FlowMediator.Contracts; +using FlowMediator.Console.Entities; +using FlowMediator.Contracts; -namespace FlowMediator.Console +namespace FlowMediator.Console.Queries { public class GetUserByIdQuery : IRequest { diff --git a/FlowMediator.Console/IUserRepository.cs b/FlowMediator.Console/Repositories/IUserRepository.cs similarity index 77% rename from FlowMediator.Console/IUserRepository.cs rename to FlowMediator.Console/Repositories/IUserRepository.cs index 534ce95..3de4ee5 100644 --- a/FlowMediator.Console/IUserRepository.cs +++ b/FlowMediator.Console/Repositories/IUserRepository.cs @@ -1,4 +1,6 @@ -namespace FlowMediator.Console +using FlowMediator.Console.Entities; + +namespace FlowMediator.Console.Repositories { public interface IUserRepository { diff --git a/FlowMediator.Console/Validators/GetUserByIdQueryValidator.cs b/FlowMediator.Console/Validators/GetUserByIdQueryValidator.cs index 2e7783c..e272bba 100644 --- a/FlowMediator.Console/Validators/GetUserByIdQueryValidator.cs +++ b/FlowMediator.Console/Validators/GetUserByIdQueryValidator.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using FlowMediator.Console.Queries; +using FluentValidation; namespace FlowMediator.Console.Validators { diff --git a/FlowMediator/Contracts/BaseEntity.cs b/FlowMediator/Contracts/BaseEntity.cs new file mode 100644 index 0000000..56893ad --- /dev/null +++ b/FlowMediator/Contracts/BaseEntity.cs @@ -0,0 +1,14 @@ +namespace FlowMediator.Contracts +{ + /// + /// Base class for domain entities that can raise domain events. + /// + public abstract class BaseEntity + { + private readonly List _domainEvents = new(); + public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); + + protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent); + public void ClearDomainEvents() => _domainEvents.Clear(); + } +} diff --git a/FlowMediator/Contracts/IDomainEvent.cs b/FlowMediator/Contracts/IDomainEvent.cs new file mode 100644 index 0000000..d23c286 --- /dev/null +++ b/FlowMediator/Contracts/IDomainEvent.cs @@ -0,0 +1,12 @@ +namespace FlowMediator.Contracts +{ + /// + /// Marker interface for domain events. + /// Domain events are treated as IRequest so they flow through mediator like commands. + /// + public interface IDomainEvent : IRequest + { + DateTime OccurredOn { get; } + } +} + diff --git a/FlowMediator/Contracts/Unit.cs b/FlowMediator/Contracts/Unit.cs new file mode 100644 index 0000000..2f039da --- /dev/null +++ b/FlowMediator/Contracts/Unit.cs @@ -0,0 +1,11 @@ +namespace FlowMediator.Contracts +{ + /// + /// Represents a void result (like MediatR.Unit). + /// + public struct Unit + { + public static readonly Unit Value = new Unit(); + } +} + diff --git a/tests/FlowMediator.Tests/DomainEventTests.cs b/tests/FlowMediator.Tests/DomainEventTests.cs new file mode 100644 index 0000000..148d98f --- /dev/null +++ b/tests/FlowMediator.Tests/DomainEventTests.cs @@ -0,0 +1,31 @@ +using FlowMediator.Contracts; +using FlowMediator.Extensions; +using FlowMediator.Tests.DomainEvents; +using Microsoft.Extensions.DependencyInjection; + +namespace FlowMediator.Tests +{ + public class DomainEventTests + { + [Fact] + public async Task Should_Handle_DomainEvent_When_EntityCreated() + { + // Arrange + var services = new ServiceCollection(); + services.AddFlowMediator(typeof(DomainEventTests).Assembly); + var provider = services.BuildServiceProvider(); + var mediator = provider.GetRequiredService(); + + var entity = TestEntity.Create("UnitTest"); + + // Act + foreach (var domainEvent in entity.DomainEvents) + { + await mediator.SendAsync(domainEvent); + } + + Assert.Single(entity.DomainEvents); + } + } + +} diff --git a/tests/FlowMediator.Tests/DomainEvents/TestEntity.cs b/tests/FlowMediator.Tests/DomainEvents/TestEntity.cs new file mode 100644 index 0000000..74f18b0 --- /dev/null +++ b/tests/FlowMediator.Tests/DomainEvents/TestEntity.cs @@ -0,0 +1,17 @@ +using FlowMediator.Contracts; + +namespace FlowMediator.Tests.DomainEvents +{ + public class TestEntity : BaseEntity + { + public string Name { get; private set; } + + public static TestEntity Create(string name) + { + var entity = new TestEntity { Name = name }; + entity.AddDomainEvent(new TestEvent($"Entity {name} created")); + return entity; + } + } + +} diff --git a/tests/FlowMediator.Tests/DomainEvents/TestEvent.cs b/tests/FlowMediator.Tests/DomainEvents/TestEvent.cs new file mode 100644 index 0000000..3477e1e --- /dev/null +++ b/tests/FlowMediator.Tests/DomainEvents/TestEvent.cs @@ -0,0 +1,13 @@ +using FlowMediator.Contracts; + +namespace FlowMediator.Tests.DomainEvents +{ + public class TestEvent : IDomainEvent + { + public string Message { get; } + public DateTime OccurredOn { get; } = DateTime.UtcNow; + + public TestEvent(string message) => Message = message; + } + +} diff --git a/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs b/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs new file mode 100644 index 0000000..12967c2 --- /dev/null +++ b/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs @@ -0,0 +1,14 @@ +using FlowMediator.Contracts; + +namespace FlowMediator.Tests.DomainEvents +{ + public class TestEventHandler : IRequestHandler + { + public Task Handle(TestEvent request, CancellationToken cancellationToken) + { + System.Console.WriteLine($"[Handler] Event triggered with message: {request.Message}"); + return Task.FromResult(Unit.Value); + } + } + +}