neozhu / CleanArchitectureWithBlazorServer

This is a repository for creating a Blazor Server dashboard application following the principles of Clean Architecture
https://architecture.blazorserver.com/
MIT License
828 stars 222 forks source link

Current domain event dispatcher does not allow atomic domain event handling. #690

Closed carlsixsmith-moj closed 4 months ago

carlsixsmith-moj commented 5 months ago

The dispatching of domain events currently won't work if you want to change the state of a different entity in an event listener.

Domain events should be consistent within a bounded context at the moment they are fired AFTER the entity has been saved and they aren't wrapped in any form of transaction.

Is this a design choice or oversite?

carlsixsmith-moj commented 4 months ago

Why has this been closed without even a comment?

neozhu commented 4 months ago

Do you have any good solutions for this? I haven't considered transactions; I just want to implement notifications to subscribers for create, update, and delete operations. Additionally, since the entity ID is auto-incremented, I need to obtain the ID of the newly created entity, as this is crucial for subsequent business operations.

carlsixsmith-moj commented 4 months ago

I could generate a pull request for this if you'd think it's worth going in.

I've got it working in our solution using the below in the interceptor model.

public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        var domainEventEntities = context!.ChangeTracker
            .Entries<IEntity>()
            .Select(po => po.Entity)
            .Where(po => po.DomainEvents.Any())
            .ToList();

        var domainEvents = domainEventEntities
            .SelectMany(x => x.DomainEvents)
            .ToList();

        if (domainEvents.Any())
        {
            await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken);
            try
            {
                var saveResult = await base.SavingChangesAsync(eventData, result, cancellationToken);

                foreach (var entity in domainEventEntities)
                {
                    entity.ClearDomainEvents();
                }

                foreach (var e in domainEvents)
                {
                    await mediator.Publish(e, cancellationToken);
                }

                await transaction.CommitAsync(cancellationToken);
                return saveResult;
            }
            catch
            {
                await transaction.RollbackAsync(cancellationToken);
                throw;
            }
        }

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

I am going to combine this with an outbox later so that we have two event types.

DomainEvents

Consumed within the same database transaction (so auto generated Ids are not a problem as the EF tracker takes care of them

Integration Events

Some of the domain event listeners will add a message to an outbox (within the same transaction as the change you are making) to be picked up later by a background process and published to an external queue.

If either or both of these are interesting to you and this project I can fork and implement it here?

neozhu commented 4 months ago

Sure, please create a pull request for this. I will review it and merge it."