From 26650a57f25e2e2d6a8a40891a21b70194e20b52 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 11:44:06 +0300 Subject: [PATCH 1/9] feat(v2): introduce explicit Event model and Publish API --- FlowMediator/Contracts/IDomainEvent.cs | 6 ++---- FlowMediator/Contracts/IEvent.cs | 8 ++++++++ FlowMediator/Contracts/IEventHandler.cs | 8 ++++++++ FlowMediator/Contracts/IMediator.cs | 13 ++++++++----- 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 FlowMediator/Contracts/IEvent.cs create mode 100644 FlowMediator/Contracts/IEventHandler.cs diff --git a/FlowMediator/Contracts/IDomainEvent.cs b/FlowMediator/Contracts/IDomainEvent.cs index d23c286..eca3a78 100644 --- a/FlowMediator/Contracts/IDomainEvent.cs +++ b/FlowMediator/Contracts/IDomainEvent.cs @@ -2,11 +2,9 @@ { /// /// Marker interface for domain events. - /// Domain events are treated as IRequest so they flow through mediator like commands. + /// Domain events represent something that already happened in the domain. /// - public interface IDomainEvent : IRequest + public interface IDomainEvent : IEvent { - DateTime OccurredOn { get; } } } - diff --git a/FlowMediator/Contracts/IEvent.cs b/FlowMediator/Contracts/IEvent.cs new file mode 100644 index 0000000..4a9dd27 --- /dev/null +++ b/FlowMediator/Contracts/IEvent.cs @@ -0,0 +1,8 @@ +namespace FlowMediator.Contracts +{ + public interface IEvent + { + Guid EventId { get; } + DateTime OccurredOn { get; } + } +} diff --git a/FlowMediator/Contracts/IEventHandler.cs b/FlowMediator/Contracts/IEventHandler.cs new file mode 100644 index 0000000..e160bd0 --- /dev/null +++ b/FlowMediator/Contracts/IEventHandler.cs @@ -0,0 +1,8 @@ +namespace FlowMediator.Contracts +{ + public interface IEventHandler + where TEvent : IEvent + { + Task Handle(TEvent @event, CancellationToken cancellationToken); + } +} diff --git a/FlowMediator/Contracts/IMediator.cs b/FlowMediator/Contracts/IMediator.cs index 2214022..d6a6f81 100644 --- a/FlowMediator/Contracts/IMediator.cs +++ b/FlowMediator/Contracts/IMediator.cs @@ -1,11 +1,14 @@ namespace FlowMediator.Contracts { - /// - /// The mediator that receives a request and dispatches it to the appropriate handler. - /// - public interface IMediator { - Task SendAsync(IRequest request, CancellationToken cancellationToken = default); + Task SendAsync( + IRequest request, + CancellationToken cancellationToken = default); + + Task PublishAsync( + TEvent @event, + CancellationToken cancellationToken = default) + where TEvent : IEvent; } } From c30f1e6e6e838689cf182cbdc3b89fe8f55e79ce Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 11:57:16 +0300 Subject: [PATCH 2/9] feat(v2): add PublishAsync implementation and event handler resolution --- FlowMediator/Core/Mediator.cs | 19 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/FlowMediator/Core/Mediator.cs b/FlowMediator/Core/Mediator.cs index da719de..863727f 100644 --- a/FlowMediator/Core/Mediator.cs +++ b/FlowMediator/Core/Mediator.cs @@ -1,4 +1,5 @@ using FlowMediator.Contracts; +using Microsoft.Extensions.DependencyInjection; namespace FlowMediator.Core { @@ -43,5 +44,23 @@ public async Task SendAsync( return await _pipelineExecutor.Execute((dynamic)request, cancellationToken, handlerDelegate); } + + public async Task PublishAsync( + TEvent @event, + CancellationToken cancellationToken = default) + where TEvent : IEvent + { + if (@event is null) + throw new ArgumentNullException(nameof(@event)); + + var handlers = _serviceProvider + .GetServices>() + .ToList(); + + foreach (var handler in handlers) + { + await handler.Handle(@event, cancellationToken); + } + } } } diff --git a/FlowMediator/Extensions/ServiceCollectionExtensions.cs b/FlowMediator/Extensions/ServiceCollectionExtensions.cs index 81a5e8a..d813d61 100644 --- a/FlowMediator/Extensions/ServiceCollectionExtensions.cs +++ b/FlowMediator/Extensions/ServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddFlowMediator(this IServiceCollection services, Assembly assembly) { services.RegisterHandlers(assembly); + services.RegisterEventHandlers(assembly); services.AddSingleton(); return services; } @@ -64,6 +65,22 @@ private static IServiceCollection RegisterBehaviors(this IServiceCollection serv return services; } - } + private static IServiceCollection RegisterEventHandlers( + this IServiceCollection services, + Assembly assembly) + { + var handlerTypes = assembly.GetTypes() + .Where(t => !t.IsAbstract && !t.IsInterface) + .SelectMany(t => t.GetInterfaces() + .Where(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IEventHandler<>)) + .Select(i => new { Handler = t, Interface = i })); + + foreach (var h in handlerTypes) + services.AddTransient(h.Interface, h.Handler); + + return services; + } + } } From 6d37951571911f644f95820187c46e5dbdb875fc Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 12:02:34 +0300 Subject: [PATCH 3/9] feat(v2): add PublishAsync implementation and event handler resolution --- FlowMediator/Extensions/ServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FlowMediator/Extensions/ServiceCollectionExtensions.cs b/FlowMediator/Extensions/ServiceCollectionExtensions.cs index d813d61..4d35031 100644 --- a/FlowMediator/Extensions/ServiceCollectionExtensions.cs +++ b/FlowMediator/Extensions/ServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ public static IServiceCollection AddFlowMediator(this IServiceCollection service public static IServiceCollection AddFlowMediatorWithBehaviors(this IServiceCollection services, Assembly assembly) { services.RegisterHandlers(assembly); + services.RegisterEventHandlers(assembly); services.RegisterBehaviors(assembly); services.AddSingleton(); return services; From e73225adf39d602484eef6118d7ee9f7fa256bf3 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 12:06:45 +0300 Subject: [PATCH 4/9] feature updated: Pipeline only applies to Send (Command / Query) --- FlowMediator/Core/Mediator.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/FlowMediator/Core/Mediator.cs b/FlowMediator/Core/Mediator.cs index 863727f..299a4d3 100644 --- a/FlowMediator/Core/Mediator.cs +++ b/FlowMediator/Core/Mediator.cs @@ -18,23 +18,27 @@ public async Task SendAsync( IRequest request, CancellationToken cancellationToken = default) { - if (request == null) + if (request is null) throw new ArgumentNullException(nameof(request)); + // Resolve handler type var handlerType = typeof(IRequestHandler<,>) .MakeGenericType(request.GetType(), typeof(TResponse)); var handler = _serviceProvider.GetService(handlerType); - if (handler == null) - throw new InvalidOperationException($"No handler found for {request.GetType().Name}"); + if (handler is null) + throw new InvalidOperationException( + $"No handler found for request type {request.GetType().Name}"); var method = handlerType.GetMethod("Handle"); - if (method == null) - throw new InvalidOperationException($"Handler {handler.GetType().Name} does not contain Handle method"); + if (method is null) + throw new InvalidOperationException( + $"Handler {handler.GetType().Name} does not contain Handle method"); RequestHandlerDelegate handlerDelegate = async () => { var result = method.Invoke(handler, new object[] { request, cancellationToken }); + if (result is Task task) return await task; @@ -42,9 +46,14 @@ public async Task SendAsync( $"Handler {handler.GetType().Name} did not return Task<{typeof(TResponse).Name}>"); }; - return await _pipelineExecutor.Execute((dynamic)request, cancellationToken, handlerDelegate); + // Pipeline only applies to Send (Command / Query) + return await _pipelineExecutor.Execute( + (dynamic)request, + cancellationToken, + handlerDelegate); } + public async Task PublishAsync( TEvent @event, CancellationToken cancellationToken = default) From 3463211eee11ba9819bf7138f03d8b272d6a26d9 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 12:22:57 +0300 Subject: [PATCH 5/9] tested: v2 features --- .../DomainEventPublishTests.cs | 29 ++++++++++++++++++ tests/FlowMediator.Tests/DomainEventTests.cs | 30 ------------------- .../DomainEvents/TestEntity.cs | 17 ----------- .../DomainEvents/TestEvent.cs | 15 +++++----- .../DomainEvents/TestEventHandler.cs | 14 ++++----- 5 files changed, 43 insertions(+), 62 deletions(-) create mode 100644 tests/FlowMediator.Tests/DomainEventPublishTests.cs delete mode 100644 tests/FlowMediator.Tests/DomainEventTests.cs delete mode 100644 tests/FlowMediator.Tests/DomainEvents/TestEntity.cs diff --git a/tests/FlowMediator.Tests/DomainEventPublishTests.cs b/tests/FlowMediator.Tests/DomainEventPublishTests.cs new file mode 100644 index 0000000..14f0208 --- /dev/null +++ b/tests/FlowMediator.Tests/DomainEventPublishTests.cs @@ -0,0 +1,29 @@ +using FlowMediator.Contracts; +using FlowMediator.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace FlowMediator.Tests +{ + public class DomainEventPublishTests + { + [Fact] + public async Task PublishAsync_Should_Invoke_EventHandlers() + { + // Arrange + var services = new ServiceCollection(); + services.AddFlowMediator(typeof(DomainEventPublishTests).Assembly); + + var provider = services.BuildServiceProvider(); + var mediator = provider.GetRequiredService(); + + var testEvent = new TestEvent("hello"); + + // Act + await mediator.PublishAsync(testEvent); + + // Assert + Assert.True(TestEventHandler.WasHandled); + } + } + +} diff --git a/tests/FlowMediator.Tests/DomainEventTests.cs b/tests/FlowMediator.Tests/DomainEventTests.cs deleted file mode 100644 index 7521afc..0000000 --- a/tests/FlowMediator.Tests/DomainEventTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 - IServiceCollection 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 deleted file mode 100644 index 74f18b0..0000000 --- a/tests/FlowMediator.Tests/DomainEvents/TestEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 index 3477e1e..4ac7d8d 100644 --- a/tests/FlowMediator.Tests/DomainEvents/TestEvent.cs +++ b/tests/FlowMediator.Tests/DomainEvents/TestEvent.cs @@ -1,13 +1,14 @@ using FlowMediator.Contracts; -namespace FlowMediator.Tests.DomainEvents +public class TestEvent : IEvent { - public class TestEvent : IDomainEvent - { - public string Message { get; } - public DateTime OccurredOn { get; } = DateTime.UtcNow; + public Guid EventId { get; } = Guid.NewGuid(); + public DateTime OccurredOn { get; } = DateTime.UtcNow; - public TestEvent(string message) => Message = message; - } + public string Message { get; } + public TestEvent(string message) + { + Message = message; + } } diff --git a/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs b/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs index 12967c2..cf653db 100644 --- a/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs +++ b/tests/FlowMediator.Tests/DomainEvents/TestEventHandler.cs @@ -1,14 +1,12 @@ using FlowMediator.Contracts; -namespace FlowMediator.Tests.DomainEvents +public class TestEventHandler : IEventHandler { - public class TestEventHandler : IRequestHandler + public static bool WasHandled { get; private set; } + + public Task Handle(TestEvent @event, CancellationToken cancellationToken) { - public Task Handle(TestEvent request, CancellationToken cancellationToken) - { - System.Console.WriteLine($"[Handler] Event triggered with message: {request.Message}"); - return Task.FromResult(Unit.Value); - } + WasHandled = true; + return Task.CompletedTask; } - } From 50632f942a9b3578c9931cfaa338855a820fd1f1 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 12:34:30 +0300 Subject: [PATCH 6/9] tested: demo --- FlowMediator.Console/Entities/TestEntity.cs | 18 ------ .../EventHandlers/TestEventHandler.cs | 15 ----- .../EventHandlers/UserCreatedEventHandler.cs | 19 ++++++ FlowMediator.Console/Events/TestEvent.cs | 13 ---- .../Events/UserCreatedEvent.cs | 18 ++++++ FlowMediator.Console/Program.cs | 59 ++++++++++--------- 6 files changed, 68 insertions(+), 74 deletions(-) delete mode 100644 FlowMediator.Console/Entities/TestEntity.cs delete mode 100644 FlowMediator.Console/EventHandlers/TestEventHandler.cs create mode 100644 FlowMediator.Console/EventHandlers/UserCreatedEventHandler.cs delete mode 100644 FlowMediator.Console/Events/TestEvent.cs create mode 100644 FlowMediator.Console/Events/UserCreatedEvent.cs diff --git a/FlowMediator.Console/Entities/TestEntity.cs b/FlowMediator.Console/Entities/TestEntity.cs deleted file mode 100644 index 7059e40..0000000 --- a/FlowMediator.Console/Entities/TestEntity.cs +++ /dev/null @@ -1,18 +0,0 @@ -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/EventHandlers/TestEventHandler.cs b/FlowMediator.Console/EventHandlers/TestEventHandler.cs deleted file mode 100644 index 8db22e9..0000000 --- a/FlowMediator.Console/EventHandlers/TestEventHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -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/EventHandlers/UserCreatedEventHandler.cs b/FlowMediator.Console/EventHandlers/UserCreatedEventHandler.cs new file mode 100644 index 0000000..f357ad3 --- /dev/null +++ b/FlowMediator.Console/EventHandlers/UserCreatedEventHandler.cs @@ -0,0 +1,19 @@ +using FlowMediator.Contracts; +using FlowMediator.Console.Events; + +namespace FlowMediator.Console.EventHandlers +{ + public class UserCreatedEventHandler + : IEventHandler + { + public Task Handle( + UserCreatedEvent @event, + CancellationToken cancellationToken) + { + System.Console.WriteLine( + $"[EVENT] User created: {@event.UserName}"); + + return Task.CompletedTask; + } + } +} diff --git a/FlowMediator.Console/Events/TestEvent.cs b/FlowMediator.Console/Events/TestEvent.cs deleted file mode 100644 index 944f612..0000000 --- a/FlowMediator.Console/Events/TestEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -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/Events/UserCreatedEvent.cs b/FlowMediator.Console/Events/UserCreatedEvent.cs new file mode 100644 index 0000000..9eb89d2 --- /dev/null +++ b/FlowMediator.Console/Events/UserCreatedEvent.cs @@ -0,0 +1,18 @@ +using FlowMediator.Contracts; + +namespace FlowMediator.Console.Events +{ + public class UserCreatedEvent : IEvent + { + public Guid EventId { get; } = Guid.NewGuid(); + public DateTime OccurredOn { get; } = DateTime.UtcNow; + + public string UserName { get; } + + public UserCreatedEvent(string userName) + { + UserName = userName + ?? throw new ArgumentNullException(nameof(userName)); + } + } +} diff --git a/FlowMediator.Console/Program.cs b/FlowMediator.Console/Program.cs index 429d0f6..ea1e655 100644 --- a/FlowMediator.Console/Program.cs +++ b/FlowMediator.Console/Program.cs @@ -1,6 +1,6 @@ -using FlowMediator.Console; -using FlowMediator.Console.Behaviors; -using FlowMediator.Console.Entities; +using FlowMediator.Console.Behaviors; +using FlowMediator.Console.Events; +using FlowMediator.Console.EventHandlers; using FlowMediator.Console.Queries; using FlowMediator.Console.Repositories; using FlowMediator.Contracts; @@ -9,56 +9,59 @@ using Microsoft.Extensions.DependencyInjection; 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(); +// --------------------------------- +// Service registration +// --------------------------------- -// handlers are automatic, behavior pipelines manual var services = new ServiceCollection(); +// Register FlowMediator (handlers auto, pipeline manual) services.AddFlowMediator(Assembly.GetExecutingAssembly()); +// App services services.AddSingleton(); + +// FluentValidation validators services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); +// Pipeline behaviors (explicit order) services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); -// ------------------------ -// Query test -// ------------------------ +// --------------------------------- +// Query test (SendAsync) +// --------------------------------- + +Console.WriteLine("---- QUERY TEST ----"); + var user = await mediator.SendAsync(new GetUserByIdQuery(1)); -System.Console.WriteLine($"User Id: {user.Id}, Name: {user.Name}"); +Console.WriteLine($"User Id: {user.Id}, Name: {user.Name}"); try { - var invalidUser = await mediator.SendAsync(new GetUserByIdQuery(0)); - System.Console.WriteLine($"User Id: {invalidUser.Id}, Name: {invalidUser.Name}"); + await mediator.SendAsync(new GetUserByIdQuery(0)); } catch (ValidationException ex) { - System.Console.WriteLine("Validation failed:"); + Console.WriteLine("Validation failed:"); foreach (var error in ex.Errors) { - System.Console.WriteLine($" - {error.PropertyName}: {error.ErrorMessage}"); + Console.WriteLine($" - {error.PropertyName}: {error.ErrorMessage}"); } } -// ------------------------ -// Domain Event test -// ------------------------ -var testEntity = TestEntity.Create("SampleEntity"); +// --------------------------------- +// Event test (PublishAsync) +// --------------------------------- +Console.WriteLine(); +Console.WriteLine("---- EVENT TEST ----"); -foreach (var domainEvent in testEntity.DomainEvents) -{ - await mediator.SendAsync(domainEvent); -} -testEntity.ClearDomainEvents(); +await mediator.PublishAsync( + new UserCreatedEvent(user.Name)); + +Console.WriteLine(); +Console.WriteLine("Demo finished successfully."); From add861cbbe13bdb7896282d630ac948ccb53b596 Mon Sep 17 00:00:00 2001 From: Berk Polat Date: Mon, 26 Jan 2026 14:42:24 +0300 Subject: [PATCH 7/9] docs: add v2 README, migration guide and release notes --- FlowMediator/Contracts/Unit.cs | 11 -- MigrationGuide.md | 63 ++++++++++ README.md | 205 ++++++++++++++------------------- ReleaseNotes.md | 58 ++++++++++ 4 files changed, 210 insertions(+), 127 deletions(-) delete mode 100644 FlowMediator/Contracts/Unit.cs create mode 100644 MigrationGuide.md create mode 100644 ReleaseNotes.md diff --git a/FlowMediator/Contracts/Unit.cs b/FlowMediator/Contracts/Unit.cs deleted file mode 100644 index 2f039da..0000000 --- a/FlowMediator/Contracts/Unit.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace FlowMediator.Contracts -{ - /// - /// Represents a void result (like MediatR.Unit). - /// - public struct Unit - { - public static readonly Unit Value = new Unit(); - } -} - diff --git a/MigrationGuide.md b/MigrationGuide.md new file mode 100644 index 0000000..75dac56 --- /dev/null +++ b/MigrationGuide.md @@ -0,0 +1,63 @@ +# Migrating from FlowMediator v1.x to v2.0 + +FlowMediator v2 introduces a more explicit and opinionated execution model. +This guide explains how to migrate existing applications. + +--- + +## 1 Domain Events + +### v1 + +```csharp +public interface IDomainEvent : IRequest { } + +await mediator.SendAsync(new UserCreatedEvent(...)); +``` + +### v2 +```csharp +public interface IDomainEvent : IEvent { } + +await mediator.PublishAsync(new UserCreatedEvent(...)); +``` + +## 2 Event Handlers + +### v1 + +```csharp +public class UserCreatedHandler + : IRequestHandler +{ + public Task Handle(...) { } +} +``` + +```csharp +public class UserCreatedHandler + : IEventHandler +{ + public Task Handle(UserCreatedEvent @event, CancellationToken ct) + { + // side effects + } +} +``` +## 3 Send vs Publish +| Purpose | Method | +| --------------------------- | -------------- | +| Command / Query | `SendAsync` | +| Domain / Integration Events | `PublishAsync` | + +Events can no longer be sent using SendAsync. + +## 4 Pipelines +- Pipelines apply only to SendAsync +- Events are executed outside the pipeline +- This enables retries, compensation, and observability in future versions + +## 5 Dependency Injection + +No changes required. +Event handlers are discovered automatically. \ No newline at end of file diff --git a/README.md b/README.md index 68c1750..2ae9681 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,53 @@ # FlowMediator -[![NuGet](https://img.shields.io/nuget/v/FlowMediator.svg)](https://www.nuget.org/packages/FlowMediator/) [![NuGet Downloads](https://img.shields.io/nuget/dt/FlowMediator.svg)](https://www.nuget.org/packages/FlowMediator/) [![Build](https://github.com/berk2k/FlowMediator/actions/workflows/ci.yml/badge.svg)](https://github.com/berk2k/FlowMediator/actions/workflows/ci.yml) +[![NuGet](https://img.shields.io/nuget/v/FlowMediator.svg)](https://www.nuget.org/packages/FlowMediator/) +[![NuGet Downloads](https://img.shields.io/nuget/dt/FlowMediator.svg)](https://www.nuget.org/packages/FlowMediator/) +[![Build](https://github.com/berk2k/FlowMediator/actions/workflows/ci.yml/badge.svg)](https://github.com/berk2k/FlowMediator/actions/workflows/ci.yml) +FlowMediator is a lightweight, opinionated mediator library for .NET 8 and .NET 9. -link: https://www.nuget.org/packages/FlowMediator/ +It focuses on **explicit application flow**, not generic messaging. -## Overview -FlowMediator is a lightweight mediator library for .NET 8/9, designed to simplify the CQRS and Mediator pattern with minimal setup. -It supports request/response messaging, pipeline behaviors, and domain events out of the box. +--- -Use it when you want: +## Why FlowMediator? --Clean separation of concerns in your application +FlowMediator was built around a simple idea: --Simple request/response messaging without boilerplate +> **Not everything is a request.** --EF Core integration with Domain Events +Commands and queries represent **intent**. +Events represent **facts that already happened**. --A minimal learning-friendly alternative to heavy frameworks +FlowMediator enforces this distinction explicitly. -## Features +--- -✅ Request/Response messaging (IRequest, IRequestHandler) +## Core Concepts -✅ Pipeline behaviors (IPipelineBehavior) for logging, validation, etc. +| Concept | Description | +|------|-----------| +| `SendAsync` | Commands & Queries (single handler, pipeline-enabled) | +| `PublishAsync` | Events (multiple handlers, side-effect oriented) | +| Pipeline | Applies only to `SendAsync` | +| Events | Never treated as requests | -✅ Domain Events with EF Core integration (v1.2.0+) - -✅ Dependency Injection (DI) extensions for IServiceCollection - -✅ Manual or automatic pipeline registration - -✅ .NET 8 and .NET 9 support +--- ## Installation + ```bash -dotnet add package FlowMediator --version 1.2.0 +dotnet add package FlowMediator ``` -## Example Usage -## 1.Define a Request & Handler -```markdown -// Query +## Request / Response Example +# Define a Request & Handler + +``` csharp public record GetUserByIdQuery(int Id) : IRequest; -// Handler -public class GetUserByIdQueryHandler : IRequestHandler +public class GetUserByIdQueryHandler + : IRequestHandler { private readonly IUserRepository _repository; @@ -54,7 +56,9 @@ public class GetUserByIdQueryHandler : IRequestHandler Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + public async Task Handle( + GetUserByIdQuery request, + CancellationToken cancellationToken) { var user = await _repository.GetByIdAsync(request.Id); return user ?? throw new Exception("User not found"); @@ -62,122 +66,91 @@ public class GetUserByIdQueryHandler : IRequestHandler(); -// Optional: pipeline behaviors -services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); -services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); +services.AddTransient( + typeof(IPipelineBehavior<,>), + typeof(LoggingBehavior<,>) +); ``` - -## 3. Use Mediator -```markdown +## Use Mediator +``` csharp var mediator = provider.GetRequiredService(); - -var user = await mediator.Send(new GetUserByIdQuery(1)); -Console.WriteLine(user.Name); // "User 1" - +var user = await mediator.SendAsync(new GetUserByIdQuery(1)); +Console.WriteLine(user.Name); ``` +## Events (v2 Model) +Events are published, not sent. -## Domain Events with EF Core -## Base Entity -```markdown -public abstract class BaseEntity +``` csharp +public class UserCreatedEvent : IEvent { - private readonly List> _domainEvents = new(); - public IReadOnlyCollection> DomainEvents => _domainEvents.AsReadOnly(); - - protected void AddDomainEvent(IRequest domainEvent) => _domainEvents.Add(domainEvent); - public void ClearDomainEvents() => _domainEvents.Clear(); + public Guid EventId { get; } = Guid.NewGuid(); + public DateTime OccurredOn { get; } = DateTime.UtcNow; } -``` -## DbContext Integration -```markdown -public class AppDbContext : DbContext +await mediator.PublishAsync(new UserCreatedEvent()); +``` +## Event Handler +``` csharp +public class UserCreatedEventHandler + : IEventHandler { - private readonly IMediator _mediator; - - public AppDbContext(DbContextOptions options, IMediator mediator) - : base(options) + public Task Handle( + UserCreatedEvent @event, + CancellationToken cancellationToken) { - _mediator = mediator; - } - - public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - var entities = ChangeTracker - .Entries() - .Where(e => e.Entity.DomainEvents.Any()) - .Select(e => e.Entity); - - foreach (var entity in entities) - { - foreach (var domainEvent in entity.DomainEvents) - { - await _mediator.Publish(domainEvent); - } - entity.ClearDomainEvents(); - } - - return await base.SaveChangesAsync(cancellationToken); + Console.WriteLine("User created"); + return Task.CompletedTask; } } - ``` -## Example Entity + Event -```markdown -public class User : BaseEntity -{ - public Guid Id { get; private set; } - public string Name { get; private set; } +## Pipelines +Pipelines apply only to SendAsync. - public User(string name) - { - Id = Guid.NewGuid(); - Name = name; - AddDomainEvent(new UserCreatedEvent(Id, Name)); - } -} +They are ideal for: -public record UserCreatedEvent(Guid Id, string Name) : IRequest; +- Logging +- Validation +- Transactions -``` +Events are executed outside the pipeline. -## Event Handler -```markdown -public class UserCreatedEventHandler : IRequestHandler -{ - public Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) - { - Console.WriteLine($"[DomainEvent] User created: {notification.Name}"); - return Task.FromResult(Unit.Value); - } -} +## When to Use FlowMediator +- Clean application flow +- Explicit CQRS +- Domain-driven design +- Predictable execution +## Roadmap +- FlowContext (CorrelationId, UserId, Metadata) +- Step-based execution model +- Observability and retry -``` +## ⚠️ Disclaimer -## 🤝 Contributing -Contributions, bug reports, and feature requests are welcome! +FlowMediator focuses on **application flow and execution semantics**. -1. Fork the repo +It does **not** provide: +- Authorization or authentication +- Security policies +- Exception handling strategies +- Infrastructure-level concerns -2. Create a feature branch (git checkout -b feature/my-feature) +These responsibilities intentionally remain in the application layer. -3. Commit changes and push +FlowMediator is designed to be **explicit and predictable**, +not a full application framework. -4. Open a Pull Request +## 🤝 Contributing +Contributions, bug reports, and feature requests are welcome. -## ⚠️ Disclaimer +## 📜 License +Licensed under the MIT License. -FlowMediator is a lightweight mediator library designed for hobby projects, learning, and simple applications. -It does **not** implement any security features by itself. -Please handle **validation, authorization, and exception management** within your own application. -## 📜 License -Licensed under the MIT License. See [LICENSE](./LICENSE) for details. diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 0000000..8a06e7b --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1,58 @@ +# FlowMediator v2.0.0 + +FlowMediator v2.0 introduces a **clear and explicit separation between application flow and event flow**. + +This release is a **conceptual milestone**. +It does not add more features — it fixes the model. + +> Events are facts, not commands. + +--- + +## Highlights + +- Explicit **Send vs Publish** model +- Events are no longer treated as requests +- Multiple event handlers supported +- Cleaner and more predictable execution flow +- Stronger foundation for observability, retries, and outbox patterns + +--- + +## Breaking Changes + +- Domain events no longer implement `IRequest` +- Events can no longer be sent via `SendAsync` +- Event handlers no longer return `Unit` +- Event execution is no longer part of the pipeline +- Compile-time separation between requests and events + +--- + +## What Stayed the Same + +- `IRequest / IRequestHandler` model +- Pipeline behaviors for `SendAsync` +- Assembly scanning and DI registration +- Manual or automatic pipeline configuration +- .NET 8 and .NET 9 support + +--- + +## Send vs Publish + +| Purpose | Method | Handlers | Pipeline | +|------|------|------|------| +| Command / Query | `SendAsync` | Single | Yes | +| Event | `PublishAsync` | Multiple | No | + +--- + +## What’s Next + +- FlowContext (CorrelationId, UserId, Metadata) +- Step-based execution model +- Built-in observability and retry support + +FlowMediator does not try to be everything. +It supports the **right things**, explicitly. From a9f7400c86d78ac43247a734c22f254cb3e71c19 Mon Sep 17 00:00:00 2001 From: berk <96010671+berk2k@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:47:36 +0300 Subject: [PATCH 8/9] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ae9681..f461d39 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ FlowMediator enforces this distinction explicitly. | Concept | Description | |------|-----------| -| `SendAsync` | Commands & Queries (single handler, pipeline-enabled) | -| `PublishAsync` | Events (multiple handlers, side-effect oriented) | +| SendAsync | Commands & Queries (single handler, pipeline-enabled) | +| PublishAsync | Events (multiple handlers, side-effect oriented) | | Pipeline | Applies only to `SendAsync` | | Events | Never treated as requests | From 43bd6959d2c9cf62a3c83024af6f68e77cbb8732 Mon Sep 17 00:00:00 2001 From: berk <96010671+berk2k@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:48:59 +0300 Subject: [PATCH 9/9] Update MigrationGuide.md --- MigrationGuide.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/MigrationGuide.md b/MigrationGuide.md index 75dac56..1d18cc1 100644 --- a/MigrationGuide.md +++ b/MigrationGuide.md @@ -5,7 +5,7 @@ This guide explains how to migrate existing applications. --- -## 1 Domain Events +## 1) Domain Events ### v1 @@ -22,10 +22,9 @@ public interface IDomainEvent : IEvent { } await mediator.PublishAsync(new UserCreatedEvent(...)); ``` -## 2 Event Handlers +## 2) Event Handlers ### v1 - ```csharp public class UserCreatedHandler : IRequestHandler @@ -33,7 +32,7 @@ public class UserCreatedHandler public Task Handle(...) { } } ``` - +### v2 ```csharp public class UserCreatedHandler : IEventHandler @@ -44,7 +43,7 @@ public class UserCreatedHandler } } ``` -## 3 Send vs Publish +## 3) Send vs Publish | Purpose | Method | | --------------------------- | -------------- | | Command / Query | `SendAsync` | @@ -52,12 +51,12 @@ public class UserCreatedHandler Events can no longer be sent using SendAsync. -## 4 Pipelines +## 4) Pipelines - Pipelines apply only to SendAsync - Events are executed outside the pipeline - This enables retries, compensation, and observability in future versions -## 5 Dependency Injection +## 5) Dependency Injection No changes required. -Event handlers are discovered automatically. \ No newline at end of file +Event handlers are discovered automatically.