diff --git a/PracticeCalendar.Api/Program.cs b/PracticeCalendar.Api/Program.cs index 9c4d85c..fae36e4 100644 --- a/PracticeCalendar.Api/Program.cs +++ b/PracticeCalendar.Api/Program.cs @@ -27,8 +27,6 @@ namespace PracticeCalendar //inject infrastructure builder.Services.AddInfrastructureServices(builder.Configuration); - //mapster - var app = builder.Build(); diff --git a/PracticeCalendar.Api/appsettings.Development.json b/PracticeCalendar.Api/appsettings.Development.json index 0c208ae..a6e86ac 100644 --- a/PracticeCalendar.Api/appsettings.Development.json +++ b/PracticeCalendar.Api/appsettings.Development.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } } diff --git a/PracticeCalendar.Api/appsettings.json b/PracticeCalendar.Api/appsettings.json index 83a9a57..9464942 100644 --- a/PracticeCalendar.Api/appsettings.json +++ b/PracticeCalendar.Api/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } }, diff --git a/PracticeCalendar.Api/practicecalendar.sqlite b/PracticeCalendar.Api/practicecalendar.sqlite index 5e6e914..951caee 100644 Binary files a/PracticeCalendar.Api/practicecalendar.sqlite and b/PracticeCalendar.Api/practicecalendar.sqlite differ diff --git a/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs b/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs new file mode 100644 index 0000000..e736f4e --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs @@ -0,0 +1,24 @@ +using MediatR; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Events; +using PracticeCalendar.Domain.Interfaces; + +namespace PracticeCalendar.Application.PracticeEvents.Events +{ + public class AttendeeAddedEventNotification : INotificationHandler> + { + private readonly IEmailSender emailSender; + + public AttendeeAddedEventNotification(IEmailSender emailSender) + { + this.emailSender = emailSender; + } + + public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) + { + var sendTo = notification.DomainEvent.AddedAtendee.EmailAddress; + await emailSender.SendEmailAsync(sendTo, "system", + "You have been added to the event", "Confirmed you have been added to the event."); + } + } +} diff --git a/PracticeCalendar.Domain/Common/DomainEventBase.cs b/PracticeCalendar.Domain/Common/DomainEventBase.cs index 3ee56bc..a6a6a07 100644 --- a/PracticeCalendar.Domain/Common/DomainEventBase.cs +++ b/PracticeCalendar.Domain/Common/DomainEventBase.cs @@ -3,5 +3,6 @@ public class DomainEventBase { public DateTime EventDate { get; protected set; } = DateTime.UtcNow; + public bool IsPublished { get; set; } } } diff --git a/PracticeCalendar.Domain/Common/DomainEventNotification.cs b/PracticeCalendar.Domain/Common/DomainEventNotification.cs new file mode 100644 index 0000000..1281805 --- /dev/null +++ b/PracticeCalendar.Domain/Common/DomainEventNotification.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace PracticeCalendar.Domain.Common +{ + public class DomainEventNotification : INotification where TDomainEvent : DomainEventBase + { + public TDomainEvent DomainEvent { get; } + + public DomainEventNotification(TDomainEvent domainEvent) + { + DomainEvent = domainEvent; + } + } +} diff --git a/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs b/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs new file mode 100644 index 0000000..e7884fc --- /dev/null +++ b/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs @@ -0,0 +1,7 @@ +namespace PracticeCalendar.Domain.Common.Interfaces +{ + public interface IDomainEventService + { + Task Publish(DomainEventBase domainEvent); + } +} diff --git a/PracticeCalendar.Domain/Entities/PracticeEvent.cs b/PracticeCalendar.Domain/Entities/PracticeEvent.cs index 80c3b3d..4f823dd 100644 --- a/PracticeCalendar.Domain/Entities/PracticeEvent.cs +++ b/PracticeCalendar.Domain/Entities/PracticeEvent.cs @@ -41,6 +41,9 @@ namespace PracticeCalendar.Domain.Entities { this.Title = title; this.Description = description; + + var titleDescUpdatedEvent = new EventUpdateTitleAndDescriptionEvent(this); + base.RegisterDomainEvent(titleDescUpdatedEvent); } public void AttendeeAcceptEvent(int attendeeId) @@ -49,6 +52,9 @@ namespace PracticeCalendar.Domain.Entities if (attendee == null) throw new InvalidAttendeeException(attendeeId); attendee.SetIsAttending(true); + + var attendeeAcceptedEvent = new AttendeeAcceptedEvent(this, attendee); + base.RegisterDomainEvent(attendeeAcceptedEvent); } public void AttendeeDeclineEvent(int attendeeId) @@ -57,6 +63,9 @@ namespace PracticeCalendar.Domain.Entities if (attendee == null) throw new InvalidAttendeeException(attendeeId); attendee.SetIsAttending(false); + + var attendeeDeclinedEvent = new AttendeeDeclinedEvent(this, attendee); + base.RegisterDomainEvent(attendeeDeclinedEvent); } } } diff --git a/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs b/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs new file mode 100644 index 0000000..a367c8d --- /dev/null +++ b/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs @@ -0,0 +1,17 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class AttendeeAcceptedEvent : DomainEventBase + { + public AttendeeAcceptedEvent(PracticeEvent practiceEvent, Attendee attendee) + { + PracticeEvent = practiceEvent; + Attendee = attendee; + } + + public PracticeEvent PracticeEvent { get; } + public Attendee Attendee { get; } + } +} diff --git a/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs b/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs new file mode 100644 index 0000000..d66cec2 --- /dev/null +++ b/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs @@ -0,0 +1,17 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class AttendeeDeclinedEvent : DomainEventBase + { + public AttendeeDeclinedEvent(PracticeEvent practiceEvent, Attendee attendee) + { + PracticeEvent = practiceEvent; + Attendee = attendee; + } + + public PracticeEvent PracticeEvent { get; } + public Attendee Attendee { get; } + } +} diff --git a/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs b/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs new file mode 100644 index 0000000..c7339c7 --- /dev/null +++ b/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs @@ -0,0 +1,15 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class EventUpdateTitleAndDescriptionEvent : DomainEventBase + { + public EventUpdateTitleAndDescriptionEvent(PracticeEvent practiceEvent) + { + PracticeEvent = practiceEvent; + } + + public PracticeEvent PracticeEvent { get; } + } +} diff --git a/PracticeCalendar.Infrastructure/ConfigureServices.cs b/PracticeCalendar.Infrastructure/ConfigureServices.cs index 5a67acc..a01c34a 100644 --- a/PracticeCalendar.Infrastructure/ConfigureServices.cs +++ b/PracticeCalendar.Infrastructure/ConfigureServices.cs @@ -5,6 +5,7 @@ using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Interfaces; using PracticeCalendar.Infrastructure.Notification; using PracticeCalendar.Infrastructure.Persistence; +using PracticeCalendar.Infrastructure.Services; namespace PracticeCalendar.Infrastructure { @@ -17,6 +18,7 @@ namespace PracticeCalendar.Infrastructure services.AddDbContext(connectionString); services.AddTransient(); + services.AddTransient(); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); diff --git a/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs b/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs index dca7d7f..8962c25 100644 --- a/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Entities; using System.Reflection; @@ -6,9 +8,15 @@ namespace PracticeCalendar.Infrastructure.Persistence { public class ApplicationDbContext : DbContext { - public ApplicationDbContext(DbContextOptions options) + private readonly IDomainEventService domainEventService; + + public DbSet Atendees => Set(); + public DbSet PracticeEvents => Set(); + + public ApplicationDbContext(DbContextOptions options, IDomainEventService domainEventService) : base(options) { + this.domainEventService = domainEventService; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -17,7 +25,30 @@ namespace PracticeCalendar.Infrastructure.Persistence modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } - public DbSet Atendees => Set(); - public DbSet PracticeEvents => Set(); + + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var events = ChangeTracker.Entries() + .Select(x => x.Entity.DomainEvents) + .SelectMany(x => x) + .Where(domainEvent => !domainEvent.IsPublished) + .ToArray(); + + var result = await base.SaveChangesAsync(cancellationToken); + + await DispatchEvents(events); + + return result; + } + + private async Task DispatchEvents(DomainEventBase[] events) + { + foreach (var @event in events) + { + @event.IsPublished = true; + await domainEventService.Publish(@event); + } + } } } diff --git a/PracticeCalendar.Infrastructure/Services/DomainEventService.cs b/PracticeCalendar.Infrastructure/Services/DomainEventService.cs new file mode 100644 index 0000000..126f000 --- /dev/null +++ b/PracticeCalendar.Infrastructure/Services/DomainEventService.cs @@ -0,0 +1,30 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Common.Interfaces; + +namespace PracticeCalendar.Infrastructure.Services +{ + public class DomainEventService : IDomainEventService + { + private readonly ILogger logger; + private readonly IPublisher mediator; + + public DomainEventService(ILogger logger, IPublisher mediator) + { + this.logger = logger; + this.mediator = mediator; + } + public async Task Publish(DomainEventBase domainEvent) + { + logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name); + await mediator.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent)); + } + + private INotification GetNotificationCorrespondingToDomainEvent(DomainEventBase domainEvent) + { + return (INotification)Activator.CreateInstance( + typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()), domainEvent)!; + } + } +}