Skip to content

Commit

Permalink
♻️ 61 migrate domain event dispatcher to use interceptor (#65)
Browse files Browse the repository at this point in the history
* Refactored base entity to use IDomainEvent interface

* Refactored DbContext to use Interceptor to handle firing of DomainEvents

* Fixed bug where saved changes was getting called twice
  • Loading branch information
danielmackay committed May 9, 2023
1 parent f0a61df commit a790e13
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,13 @@ private async Task<bool> BeUniqueTitle(string title, CancellationToken cancellat
public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, Guid>
{
private readonly IMapper _mapper;
private readonly IPublisher _publisher;
private readonly IRepositoryBase<TodoItem> _repository;

public CreateTodoItemCommandHandler(
IMapper mapper,
IPublisher publisher,
IRepositoryBase<TodoItem> repository)
{
_mapper = mapper;
_publisher = publisher;
_repository = repository;
}

Expand All @@ -50,12 +48,11 @@ public async Task<Guid> Handle(
CancellationToken cancellationToken)
{
var todoItem = _mapper.Map<TodoItem>(request);

todoItem.AddDomainEvent(new TodoItemCreatedEvent(todoItem));

await _repository.AddAsync(todoItem, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);


return todoItem.Id.Value;
}
}
14 changes: 7 additions & 7 deletions src/Domain/Common/BaseEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

namespace Domain.Common;

public abstract class BaseEntity : AuditableEntity
public abstract class BaseEntity<TId> : AuditableEntity, IDomainEvents
{
private readonly List<BaseEvent> _domainEvents = new();


public TId Id { get; set; } = default!;

[NotMapped]
public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();

public void AddDomainEvent(BaseEvent domainEvent) => _domainEvents.Add(domainEvent);

public void RemoveDomainEvent(BaseEvent domainEvent) => _domainEvents.Remove(domainEvent);

public void ClearDomainEvents() => _domainEvents.Clear();
}

public abstract class BaseEntity<TId> : BaseEntity
{
public TId Id { get; set; } = default!;
}
12 changes: 12 additions & 0 deletions src/Domain/Common/IDomainEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Domain.Common;

public interface IDomainEvents
{
IReadOnlyCollection<BaseEvent> DomainEvents { get; }

void AddDomainEvent(BaseEvent domainEvent);

void ClearDomainEvents();

void RemoveDomainEvent(BaseEvent domainEvent);
}
28 changes: 0 additions & 28 deletions src/Infrastructure/Common/MediatorExtensions.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class DependencyInjection
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<EntitySaveChangesInterceptor>();
services.AddScoped<DispatchDomainEventsInterceptor>();

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("DefaultConnection"), builder =>
Expand Down
2 changes: 1 addition & 1 deletion src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
Expand Down
22 changes: 9 additions & 13 deletions src/Infrastructure/Persistence/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Domain.Entities;
using Infrastructure.Common;
using Infrastructure.Persistence.Interceptors;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Reflection;

Expand All @@ -10,12 +8,16 @@ namespace Infrastructure.Persistence;
public class ApplicationDbContext : DbContext
{
private readonly EntitySaveChangesInterceptor _saveChangesInterceptor;
private readonly IMediator _mediator;
private readonly DispatchDomainEventsInterceptor _dispatchDomainEventsInterceptor;

public ApplicationDbContext(DbContextOptions options, EntitySaveChangesInterceptor saveChangesInterceptor, IMediator mediator) : base(options)
public ApplicationDbContext(
DbContextOptions options,
EntitySaveChangesInterceptor saveChangesInterceptor,
DispatchDomainEventsInterceptor dispatchDomainEventsInterceptor)
: base(options)
{
_saveChangesInterceptor = saveChangesInterceptor;
_mediator = mediator;
_dispatchDomainEventsInterceptor = dispatchDomainEventsInterceptor;
}

public DbSet<TodoItem> TodoItems => Set<TodoItem>();
Expand All @@ -29,13 +31,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(_saveChangesInterceptor);
}

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
await _mediator.DispatchDomainEvents(this);

return await base.SaveChangesAsync(cancellationToken);
// Order of the interceptors is important
optionsBuilder.AddInterceptors(_saveChangesInterceptor, _dispatchDomainEventsInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Domain.Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Infrastructure.Persistence.Interceptors;

public class DispatchDomainEventsInterceptor : SaveChangesInterceptor
{
private readonly IMediator _mediator;

public DispatchDomainEventsInterceptor(IMediator mediator)
{
_mediator = mediator;
}

public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
DispatchDomainEvents(eventData.Context).ConfigureAwait(false).GetAwaiter().GetResult();

return base.SavingChanges(eventData, result);
}

public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
await DispatchDomainEvents(eventData.Context);

return await base.SavingChangesAsync(eventData, result, cancellationToken);
}

public async Task DispatchDomainEvents(DbContext? context)
{
if (context is null)
return;

var entities = context.ChangeTracker
.Entries<IDomainEvents>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity)
.ToList();

var domainEvents = entities
.SelectMany(e => e.DomainEvents)
.ToList();

entities.ForEach(e => e.ClearDomainEvents());

foreach (var domainEvent in domainEvents)
await _mediator.Publish(domainEvent);
}
}

0 comments on commit a790e13

Please sign in to comment.