From 070c3117266dd3fd10413f189e19740a6b728190 Mon Sep 17 00:00:00 2001 From: Kamran Sadin Date: Wed, 13 Aug 2025 22:35:15 +0330 Subject: [PATCH] Fix DomainEvents --- .../PostCreatedDomainEventHandler.cs | 27 ++++++++++++++ .../Project.Application.csproj | 4 +- Samples/BlogApp/Project.ClientApp/Program.cs | 5 +-- .../Project.ClientApp.csproj | 2 +- .../Project.Common/Project.Common.csproj | 4 +- .../Blog/Events/PostCreatedDomainEvent.cs | 17 +++++++++ .../Project.Domain/Aggregates/Blog/Post.cs | 2 + .../Project.Domain/Project.Domain.csproj | 4 +- .../Data/ApplicationDbContext.cs | 36 +++++++++++++++++- .../DependencyInjection.cs | 4 +- .../Project.Infrastructure.csproj | 21 +++++------ .../Services/DomainEventDispatcher.cs | 37 +++++++++++++++++++ .../Project.Presentation.csproj | 4 +- .../Project.WebApi/Project.WebApi.csproj | 4 +- .../appsettings.Development.json | 4 +- .../KSDomain/DomainEventDispatcher.cs | 36 ++++++++++++++++++ .../KSDomain/IDomainEventDispatcher.cs | 6 +++ 17 files changed, 187 insertions(+), 30 deletions(-) create mode 100644 Samples/BlogApp/Project.Application/Blog/EventHandlers/PostCreatedDomainEventHandler.cs create mode 100644 Samples/BlogApp/Project.Domain/Aggregates/Blog/Events/PostCreatedDomainEvent.cs create mode 100644 Samples/BlogApp/Project.Infrastructure/Services/DomainEventDispatcher.cs create mode 100644 src/KSFramework/KSDomain/DomainEventDispatcher.cs create mode 100644 src/KSFramework/KSDomain/IDomainEventDispatcher.cs diff --git a/Samples/BlogApp/Project.Application/Blog/EventHandlers/PostCreatedDomainEventHandler.cs b/Samples/BlogApp/Project.Application/Blog/EventHandlers/PostCreatedDomainEventHandler.cs new file mode 100644 index 0000000..500e435 --- /dev/null +++ b/Samples/BlogApp/Project.Application/Blog/EventHandlers/PostCreatedDomainEventHandler.cs @@ -0,0 +1,27 @@ +using KSFramework.KSMessaging.Abstraction; +using Microsoft.Extensions.Logging; +using Project.Domain.Aggregates.Blog.Events; + +namespace Project.Application.Blog.EventHandlers; + +public sealed class PostCreatedDomainEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public PostCreatedDomainEventHandler(ILogger logger) + { + _logger = logger; + } + + public Task Handle(PostCreatedDomainEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation("Post created: {PostId} - {Title}", notification.PostId, notification.Title); + + // Here you can add additional business logic like: + // - Sending notifications + // - Updating read models + // - Triggering external systems + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Samples/BlogApp/Project.Application/Project.Application.csproj b/Samples/BlogApp/Project.Application/Project.Application.csproj index b9545dc..ba6de9b 100644 --- a/Samples/BlogApp/Project.Application/Project.Application.csproj +++ b/Samples/BlogApp/Project.Application/Project.Application.csproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0 enable enable diff --git a/Samples/BlogApp/Project.ClientApp/Program.cs b/Samples/BlogApp/Project.ClientApp/Program.cs index 1510d12..7f168cb 100644 --- a/Samples/BlogApp/Project.ClientApp/Program.cs +++ b/Samples/BlogApp/Project.ClientApp/Program.cs @@ -18,12 +18,11 @@ app.UseAuthorization(); -app.MapStaticAssets(); +app.UseStaticFiles(); app.MapControllerRoute( name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}") - .WithStaticAssets(); + pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); diff --git a/Samples/BlogApp/Project.ClientApp/Project.ClientApp.csproj b/Samples/BlogApp/Project.ClientApp/Project.ClientApp.csproj index a3a34b6..1b28a01 100644 --- a/Samples/BlogApp/Project.ClientApp/Project.ClientApp.csproj +++ b/Samples/BlogApp/Project.ClientApp/Project.ClientApp.csproj @@ -1,7 +1,7 @@ - net10.0 + net8.0 enable enable diff --git a/Samples/BlogApp/Project.Common/Project.Common.csproj b/Samples/BlogApp/Project.Common/Project.Common.csproj index da503eb..d2c0af5 100644 --- a/Samples/BlogApp/Project.Common/Project.Common.csproj +++ b/Samples/BlogApp/Project.Common/Project.Common.csproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0 enable enable diff --git a/Samples/BlogApp/Project.Domain/Aggregates/Blog/Events/PostCreatedDomainEvent.cs b/Samples/BlogApp/Project.Domain/Aggregates/Blog/Events/PostCreatedDomainEvent.cs new file mode 100644 index 0000000..9fb6df0 --- /dev/null +++ b/Samples/BlogApp/Project.Domain/Aggregates/Blog/Events/PostCreatedDomainEvent.cs @@ -0,0 +1,17 @@ +using KSFramework.KSDomain; + +namespace Project.Domain.Aggregates.Blog.Events; + +public sealed class PostCreatedDomainEvent : IDomainEvent +{ + public PostCreatedDomainEvent(Guid postId, string title) + { + PostId = postId; + Title = title; + OccurredOn = DateTime.UtcNow; + } + + public Guid PostId { get; } + public string Title { get; } + public DateTime OccurredOn { get; } +} \ No newline at end of file diff --git a/Samples/BlogApp/Project.Domain/Aggregates/Blog/Post.cs b/Samples/BlogApp/Project.Domain/Aggregates/Blog/Post.cs index 1de7838..06bf5d4 100644 --- a/Samples/BlogApp/Project.Domain/Aggregates/Blog/Post.cs +++ b/Samples/BlogApp/Project.Domain/Aggregates/Blog/Post.cs @@ -4,6 +4,7 @@ using KSFramework.Utilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Project.Domain.Aggregates.Blog.Events; namespace Project.Domain.Aggregates.Blog; @@ -44,6 +45,7 @@ public static Post Create(string title, string content) Id = Guid.NewGuid(), }; + post.AddDomainEvent(new PostCreatedDomainEvent(post.Id, post.Title)); return post; } diff --git a/Samples/BlogApp/Project.Domain/Project.Domain.csproj b/Samples/BlogApp/Project.Domain/Project.Domain.csproj index 4bc5765..c0312b0 100644 --- a/Samples/BlogApp/Project.Domain/Project.Domain.csproj +++ b/Samples/BlogApp/Project.Domain/Project.Domain.csproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0 enable enable diff --git a/Samples/BlogApp/Project.Infrastructure/Data/ApplicationDbContext.cs b/Samples/BlogApp/Project.Infrastructure/Data/ApplicationDbContext.cs index ab4a99d..9d2b5c2 100644 --- a/Samples/BlogApp/Project.Infrastructure/Data/ApplicationDbContext.cs +++ b/Samples/BlogApp/Project.Infrastructure/Data/ApplicationDbContext.cs @@ -2,14 +2,24 @@ using KSFramework.Utilities; using Microsoft.EntityFrameworkCore; using Project.Infrastructure.Outbox; +using Microsoft.Extensions.DependencyInjection; +using Project.Infrastructure.Services; +using System.Linq; namespace Project.Infrastructure.Data; public class ApplicationDbContext : DbContext { - public ApplicationDbContext(DbContextOptions options) - :base(options) + private readonly IDomainEventDispatcher _dispatcher; + private readonly IServiceProvider _serviceProvider; + + public ApplicationDbContext( + DbContextOptions options, + IServiceProvider serviceProvider) + : base(options) { + _serviceProvider = serviceProvider; + _dispatcher = _serviceProvider.GetRequiredService(); } public DbSet OutboxMessages { get; set; } @@ -48,4 +58,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) #endregion } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var domainEvents = ChangeTracker.Entries() + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + var result = await base.SaveChangesAsync(cancellationToken); + + if (domainEvents.Any()) + { + await _dispatcher.DispatchEventsAsync(domainEvents, cancellationToken); + + // Clear domain events after dispatching + foreach (var entry in ChangeTracker.Entries()) + { + entry.Entity.ClearDomainEvents(); + } + } + + return result; + } } \ No newline at end of file diff --git a/Samples/BlogApp/Project.Infrastructure/DependencyInjection.cs b/Samples/BlogApp/Project.Infrastructure/DependencyInjection.cs index 57ece63..f4d7c94 100644 --- a/Samples/BlogApp/Project.Infrastructure/DependencyInjection.cs +++ b/Samples/BlogApp/Project.Infrastructure/DependencyInjection.cs @@ -1,4 +1,4 @@ -using KSFramework.GenericRepository; +using KSFramework.GenericRepository; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -18,6 +18,8 @@ public static IServiceCollection RegisterInfrastructure(this IServiceCollection x => x.MigrationsAssembly("Project.Infrastructure")); }); + + services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; diff --git a/Samples/BlogApp/Project.Infrastructure/Project.Infrastructure.csproj b/Samples/BlogApp/Project.Infrastructure/Project.Infrastructure.csproj index b9545dc..62b5bf2 100644 --- a/Samples/BlogApp/Project.Infrastructure/Project.Infrastructure.csproj +++ b/Samples/BlogApp/Project.Infrastructure/Project.Infrastructure.csproj @@ -1,15 +1,14 @@ - + - - net10.0 - enable - enable - + + net8.0 + enable + enable + - - - - - + + + + diff --git a/Samples/BlogApp/Project.Infrastructure/Services/DomainEventDispatcher.cs b/Samples/BlogApp/Project.Infrastructure/Services/DomainEventDispatcher.cs new file mode 100644 index 0000000..7184145 --- /dev/null +++ b/Samples/BlogApp/Project.Infrastructure/Services/DomainEventDispatcher.cs @@ -0,0 +1,37 @@ +using KSFramework.KSDomain; +using KSFramework.KSMessaging.Abstraction; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System; + +namespace Project.Infrastructure.Services; + +public sealed class DomainEventDispatcher : IDomainEventDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public DomainEventDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task DispatchEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default) + { + foreach (var domainEvent in domainEvents) + { + var handlerType = typeof(INotificationHandler<>).MakeGenericType(domainEvent.GetType()); + + using var scope = _serviceProvider.CreateScope(); + var handlers = scope.ServiceProvider.GetServices(handlerType); + + foreach (var handler in handlers) + { + await (Task)handlerType + .GetMethod("Handle") + ?.Invoke(handler, new object[] { domainEvent, cancellationToken })!; + } + } + } +} \ No newline at end of file diff --git a/Samples/BlogApp/Project.Presentation/Project.Presentation.csproj b/Samples/BlogApp/Project.Presentation/Project.Presentation.csproj index 1a3a86c..91a9fd0 100644 --- a/Samples/BlogApp/Project.Presentation/Project.Presentation.csproj +++ b/Samples/BlogApp/Project.Presentation/Project.Presentation.csproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0 enable enable diff --git a/Samples/BlogApp/Project.WebApi/Project.WebApi.csproj b/Samples/BlogApp/Project.WebApi/Project.WebApi.csproj index 2bef60c..d8a8ea4 100644 --- a/Samples/BlogApp/Project.WebApi/Project.WebApi.csproj +++ b/Samples/BlogApp/Project.WebApi/Project.WebApi.csproj @@ -1,13 +1,13 @@ - net10.0 + net8.0 enable enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Samples/BlogApp/Project.WebApi/appsettings.Development.json b/Samples/BlogApp/Project.WebApi/appsettings.Development.json index 0e4ec3d..35410c7 100644 --- a/Samples/BlogApp/Project.WebApi/appsettings.Development.json +++ b/Samples/BlogApp/Project.WebApi/appsettings.Development.json @@ -1,8 +1,8 @@ { "ConnectionStrings": { - "Default": "Server=localhost,1433;Initial Catalog=TestKSFrameworkBlogDb;User ID=sa;Password=SqlPassword123;TrustServerCertificate=true", - "Master": "Server=localhost,1433;Initial Catalog=Master;User ID=sa;Password=SqlPassword123;TrustServerCertificate=true" + "Default": "Server=localhost,1433;Initial Catalog=TestKSFrameworkBlogDb;User ID=sa;Password=reallyStrongPwd123;TrustServerCertificate=true", + "Master": "Server=localhost,1433;Initial Catalog=Master;User ID=sa;Password=reallyStrongPwd123;TrustServerCertificate=true" }, "Logging": { "LogLevel": { diff --git a/src/KSFramework/KSDomain/DomainEventDispatcher.cs b/src/KSFramework/KSDomain/DomainEventDispatcher.cs new file mode 100644 index 0000000..d9259dc --- /dev/null +++ b/src/KSFramework/KSDomain/DomainEventDispatcher.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using KSFramework.KSMessaging.Abstraction; +using Microsoft.Extensions.DependencyInjection; + +namespace KSFramework.KSDomain; + +public sealed class DomainEventDispatcher : IDomainEventDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public DomainEventDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task DispatchEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default) + { + foreach (var domainEvent in domainEvents) + { + var handlerType = typeof(INotificationHandler<>).MakeGenericType(domainEvent.GetType()); + + using var scope = _serviceProvider.CreateScope(); + var handlers = scope.ServiceProvider.GetServices(handlerType); + + foreach (var handler in handlers) + { + await (Task)handlerType + .GetMethod("Handle") + ?.Invoke(handler, new object[] { domainEvent, cancellationToken })!; + } + } + } +} \ No newline at end of file diff --git a/src/KSFramework/KSDomain/IDomainEventDispatcher.cs b/src/KSFramework/KSDomain/IDomainEventDispatcher.cs new file mode 100644 index 0000000..1c2d3c6 --- /dev/null +++ b/src/KSFramework/KSDomain/IDomainEventDispatcher.cs @@ -0,0 +1,6 @@ +namespace KSFramework.KSDomain; + +public interface IDomainEventDispatcher +{ + Task DispatchEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default); +} \ No newline at end of file