Skip to content

Commit

Permalink
Add Idempotent domain event handler implementation for user module
Browse files Browse the repository at this point in the history
  • Loading branch information
Kishotta committed May 29, 2024
1 parent 3573322 commit edbf2c1
Show file tree
Hide file tree
Showing 24 changed files with 779 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ private static void ApplyMigrations<TDbContext>(IServiceScope scope)

context.Database.Migrate();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Evently.Common.Domain;

namespace Evently.Common.Application.Messaging;

public abstract class DomainEventHandler<TDomainEvent> : IDomainEventHandler, IDomainEventHandler<TDomainEvent>
where TDomainEvent : IDomainEvent
{
public Task Handle(IDomainEvent domainEvent, CancellationToken cancellationToken = default)
{
return Handle((TDomainEvent)domainEvent, cancellationToken);
}

public abstract Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
using Evently.Common.Domain;
using MediatR;

namespace Evently.Common.Application.Messaging;

public interface IDomainEventHandler<in TDomainEvent> : INotificationHandler<TDomainEvent>
where TDomainEvent : IDomainEvent;
public interface IDomainEventHandler
{
Task Handle(IDomainEvent domainEvent, CancellationToken cancellationToken = default);
}

public interface IDomainEventHandler<in TDomainEvent>
where TDomainEvent : IDomainEvent
{
Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
<PackageReference Include="Scrutor" Version="4.2.2" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Concurrent;
using System.Reflection;
using Evently.Common.Application.Messaging;
using Microsoft.Extensions.DependencyInjection;

namespace Evently.Common.Infrastructure.Outbox;

public static class DomainEventHandlersFactory
{
private static readonly ConcurrentDictionary<string, Type[]> HandlersDictionary = new();

public static IEnumerable<IDomainEventHandler> GetHandlers(
Type domainEventType,
IServiceProvider serviceProvider,
Assembly assembly)
{
var domainEventHandlerTypes = HandlersDictionary.GetOrAdd(
$"{assembly.GetName().Name}{domainEventType.Name}",
_ => assembly.GetTypes()
.Where(handlerType => handlerType.IsAssignableTo(typeof(IDomainEventHandler<>).MakeGenericType(domainEventType)))
.ToArray());

return domainEventHandlerTypes
.Select(serviceProvider.GetRequiredService)
.Select(domainEventHandler => (domainEventHandler as IDomainEventHandler)!)
.ToList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Evently.Common.Infrastructure.Outbox;

public sealed class OutboxMessageConsumer(Guid outboxMessageId, string name)
{
public Guid OutboxMessageId { get; init; } = outboxMessageId;
public string Name { get; init; } = name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Evently.Common.Infrastructure.Outbox;

public sealed class OutboxMessageConsumerConfiguration : IEntityTypeConfiguration<OutboxMessageConsumer>
{
public void Configure(EntityTypeBuilder<OutboxMessageConsumer> builder)
{
builder.ToTable("outbox_message_consumers");

builder.HasKey(outboxMessageConsumer => new { outboxMessageConsumer.OutboxMessageId, outboxMessageConsumer.Name });

builder.Property(outboxMessageConsumer => outboxMessageConsumer.OutboxMessageId).HasMaxLength(500);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ protected static void AssertDomainEventHandlersShouldNotBePublic(Assembly applic
Types.InAssembly(applicationAssembly)
.That()
.ImplementInterface(typeof(IDomainEventHandler<>))
.Or()
.Inherit(typeof(DomainEventHandler<>))
.Should()
.NotBePublic()
.GetResult()
Expand All @@ -176,6 +178,8 @@ protected static void AssertDomainEventHandlersShouldBeSealed(Assembly applicati
Types.InAssembly(applicationAssembly)
.That()
.ImplementInterface(typeof(IDomainEventHandler<>))
.Or()
.Inherit(typeof(DomainEventHandler<>))
.Should()
.BeSealed()
.GetResult()
Expand All @@ -187,6 +191,8 @@ protected static void AssertDomainEventHandlersShouldHaveNameEndingWithDomainEve
Types.InAssembly(applicationAssembly)
.That()
.ImplementInterface(typeof(IDomainEventHandler<>))
.Or()
.Inherit(typeof(DomainEventHandler<>))
.Should()
.HaveNameEndingWith("DomainEventHandler")
.GetResult()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Evently.Common.Application.Messaging;
using Evently.Common.Infrastructure.Database;
using Evently.Common.Presentation.Endpoints;
using Evently.Modules.Attendance.Application.Abstractions.Authentication;
Expand All @@ -13,16 +14,23 @@
using MassTransit;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Evently.Modules.Attendance.Infrastructure;

public static class AttendanceModule
{
public static IServiceCollection AddAttendanceModule(this IServiceCollection services, IConfiguration configuration) =>
public static IServiceCollection AddAttendanceModule(this IServiceCollection services, IConfiguration configuration)
{
services.AddDomainEventHandlers();

services
.AddInfrastructure(configuration)
.AddEndpoints(Presentation.AssemblyReference.Assembly);


return services;
}

private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
services
.AddScoped<IAttendanceContext, AttendanceContext>()
Expand All @@ -40,4 +48,11 @@ public static void ConfigureConsumers(IRegistrationConfigurator registrationConf
{
registrationConfigurator.AddConsumers(Presentation.AssemblyReference.Assembly);
}

private static void AddDomainEventHandlers(this IServiceCollection services) =>
Application.AssemblyReference.Assembly
.GetTypes()
.Where(type => type.IsAssignableTo(typeof(IDomainEventHandler)))
.ToList()
.ForEach(services.TryAddScoped);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace Evently.Modules.Events.Application.Events.PublishEvent;
internal sealed class EventPublishedDomainEventHandler(
ISender sender,
IEventBus eventBus)
: IDomainEventHandler<EventPublishedDomainEvent>
: DomainEventHandler<EventPublishedDomainEvent>
{
public async Task Handle(EventPublishedDomainEvent notification, CancellationToken cancellationToken)
public override async Task Handle(EventPublishedDomainEvent notification, CancellationToken cancellationToken = default)
{
var result = await sender.Send(new GetEventQuery(notification.EventId), cancellationToken);
if (result.IsFailure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
namespace Evently.Modules.Events.Application.Events.RescheduleEvent;

internal sealed class EventRescheduledDomainEventHandler(ILogger<EventRescheduledDomainEventHandler> logger)
: IDomainEventHandler<EventRescheduledDomainEvent>
: DomainEventHandler<EventRescheduledDomainEvent>
{
public Task Handle(EventRescheduledDomainEvent notification, CancellationToken cancellationToken)
public override Task Handle(EventRescheduledDomainEvent notification, CancellationToken cancellationToken = default)
{
logger.LogInformation("Event rescheduled: {EventId}", notification.EventId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Evently.Common.Application.Messaging;
using Evently.Common.Infrastructure.Database;
using Evently.Common.Presentation.Endpoints;
using Evently.Modules.Events.Application.Abstractions.Data;
Expand All @@ -10,15 +11,22 @@
using Evently.Modules.Events.Infrastructure.TicketTypes;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Evently.Modules.Events.Infrastructure;

public static class EventsModule
{
public static IServiceCollection AddEventsModule(this IServiceCollection services, IConfiguration configuration) =>
public static IServiceCollection AddEventsModule(this IServiceCollection services, IConfiguration configuration)
{
services.AddDomainEventHandlers();

services
.AddEndpoints(Presentation.AssemblyReference.Assembly)
.AddInfrastructure(configuration);
.AddInfrastructure(configuration)
.AddEndpoints(Presentation.AssemblyReference.Assembly);

return services;
}

private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
services
Expand All @@ -28,5 +36,10 @@ private static IServiceCollection AddInfrastructure(this IServiceCollection serv
.AddScoped<ICategoryRepository, CategoryRepository>()
.AddScoped<ITicketTypeRepository, TicketTypeRepository>();


private static void AddDomainEventHandlers(this IServiceCollection services) =>
Application.AssemblyReference.Assembly
.GetTypes()
.Where(type => type.IsAssignableTo(typeof(IDomainEventHandler)))
.ToList()
.ForEach(services.TryAddScoped);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace Evently.Modules.Ticketing.Application.TicketTypes;

internal sealed class TicketTypeSoldOutDomainEventHandler(IEventBus eventBus)
: IDomainEventHandler<TicketTypeSoldOutDomainEvent>
: DomainEventHandler<TicketTypeSoldOutDomainEvent>
{
public async Task Handle(TicketTypeSoldOutDomainEvent domainEvent, CancellationToken cancellationToken)
public override async Task Handle(TicketTypeSoldOutDomainEvent domainEvent, CancellationToken cancellationToken = default)
{
await eventBus.PublishAsync(
new TicketTypeSoldOutIntegrationEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace Evently.Modules.Ticketing.Application.Tickets.ArchiveTicket;

internal sealed class TicketArchivedDomainEventHandler(IEventBus eventBus) : IDomainEventHandler<TicketArchivedDomainEvent>
internal sealed class TicketArchivedDomainEventHandler(IEventBus eventBus)
: DomainEventHandler<TicketArchivedDomainEvent>
{
public async Task Handle(TicketArchivedDomainEvent domainEvent, CancellationToken cancellationToken)
public override async Task Handle(TicketArchivedDomainEvent domainEvent, CancellationToken cancellationToken = default)
{
await eventBus.PublishAsync(
new TicketArchivedIntegrationEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
namespace Evently.Modules.Ticketing.Application.Tickets.CreateTicketBatch;

internal sealed class TicketCreatedDomainEventHandler(ISender sender, IEventBus eventBus)
: IDomainEventHandler<TicketCreatedDomainEvent>
: DomainEventHandler<TicketCreatedDomainEvent>
{
public async Task Handle(TicketCreatedDomainEvent domainEvent, CancellationToken cancellationToken)
public override async Task Handle(TicketCreatedDomainEvent domainEvent, CancellationToken cancellationToken = default)
{
var result = await sender.Send(new GetTicketQuery(domainEvent.TicketId), cancellationToken);
if (result.IsFailure)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Evently.Common.Application.Messaging;
using Evently.Common.Infrastructure.Database;
using Evently.Common.Presentation.Endpoints;
using Evently.Modules.Ticketing.Application.Abstractions.Data;
Expand All @@ -17,16 +18,23 @@
using MassTransit;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Evently.Modules.Ticketing.Infrastructure;

public static class TicketingModule
{
public static IServiceCollection AddTicketingModule(this IServiceCollection services, IConfiguration configuration) =>
public static IServiceCollection AddTicketingModule(this IServiceCollection services, IConfiguration configuration)
{
services.AddDomainEventHandlers();

services
.AddInfrastructure(configuration)
.AddEndpoints(Presentation.AssemblyReference.Assembly);

return services;
}

private static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
services
.AddSingleton<CartService>()
Expand All @@ -48,4 +56,11 @@ public static void ConfigureConsumers(IRegistrationConfigurator registrationConf
{
registrationConfigurator.AddConsumers(Presentation.AssemblyReference.Assembly);
}

private static void AddDomainEventHandlers(this IServiceCollection services) =>
Application.AssemblyReference.Assembly
.GetTypes()
.Where(type => type.IsAssignableTo(typeof(IDomainEventHandler)))
.ToList()
.ForEach(services.TryAddScoped);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace Evently.Modules.Users.Application.Users.ChangeUserName;

internal sealed class UserNameChangedDomainEventHandler (
IEventBus eventBus)
: IDomainEventHandler<UserNameChangedDomainEvent>
: DomainEventHandler<UserNameChangedDomainEvent>
{
public async Task Handle(UserNameChangedDomainEvent notification, CancellationToken cancellationToken)
public override async Task Handle(UserNameChangedDomainEvent notification, CancellationToken cancellationToken = default)
{
await eventBus.PublishAsync(
new UserNameChangedIntegrationEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace Evently.Modules.Users.Application.Users.RegisterUser;
internal sealed class UserRegisteredDomainEventHandler(
ISender sender,
IEventBus eventBus)
: IDomainEventHandler<UserRegisteredDomainEvent>
: DomainEventHandler<UserRegisteredDomainEvent>
{
public async Task Handle(UserRegisteredDomainEvent notification, CancellationToken cancellationToken)
public override async Task Handle(UserRegisteredDomainEvent notification, CancellationToken cancellationToken = default)
{
var result = await sender.Send(new GetUserQuery(notification.UserId), cancellationToken);
if (result.IsFailure)
Expand Down
Loading

0 comments on commit edbf2c1

Please sign in to comment.