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."); 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; } } 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/FlowMediator/Core/Mediator.cs b/FlowMediator/Core/Mediator.cs index da719de..299a4d3 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 { @@ -17,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; @@ -41,7 +46,30 @@ 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) + 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..4d35031 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; } @@ -27,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; @@ -64,6 +66,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; + } + } } diff --git a/MigrationGuide.md b/MigrationGuide.md new file mode 100644 index 0000000..1d18cc1 --- /dev/null +++ b/MigrationGuide.md @@ -0,0 +1,62 @@ +# 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(...) { } +} +``` +### v2 +```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. diff --git a/README.md b/README.md index 68c1750..f461d39 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. 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; } - }