Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle domain events that are raised by event handlers?

I've implemented the following pattern by jbogard:

http://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-pattern/

Imagine the following entity Coupon and event CouponActivatedEvent:

public class Coupon : DomainEntity
{
    public virtual User User { get; private set; }

    // ...omitted...

    public void Activate(User user)
    {
        if (User != null)
            throw new InvalidOperationException("Coupon already activated");

        User = user;

        Events.Add(new CouponActivatedEvent(this));
    }
}

The following event handler CouponActivatedHandler:

public class CouponActivatedHandler : IDomainEventHandler<CouponActivatedEvent>
{
    public void Handle(CouponActivatedEvent e)
    {
        // user gets 5 credits because coupon was activated
        for (int i = 0; i < 5; i++)
        {
            e.Coupon.User.AddCredit(); // raises UserReceivedCreditEvent and CreditCreatedEvent
        }
    }
}

The following SaveChanges override on DbContext (Entity Framework 6), taken from jbogard's blog post:

public override int SaveChanges()
{
    var domainEventEntities = ChangeTracker.Entries<IDomainEntity>()
        .Select(po => po.Entity)
        .Where(po => po.Events.Any())
        .ToArray();

    foreach (var entity in domainEventEntities)
    {
        var events = entity.Events.ToArray();
        entity.Events.Clear();
        foreach (var domainEvent in events)
        {
            _dispatcher.Dispatch(domainEvent);
        }
    }

    return base.SaveChanges();
}

If we now activate a coupon, this will raise the CouponActivatedEvent. When calling SaveChanges, the handler will be executed, and UserReceivedCreditEvent and CreditCreatedEvent will be raised. They won't be handled though. Am I misunderstanding the pattern? Or is the SaveChanges override not appropriate?

I've considered creating a loop that will repeat until no new events are raised before moving on to base.SaveChanges();... but I'm worried that I will create endless loops accidentally. Like so:

public override int SaveChanges()
{
    do 
    {
        var domainEventEntities = ChangeTracker.Entries<IDomainEntity>()
            .Select(po => po.Entity)
            .Where(po => po.Events.Any())
            .ToArray();

        foreach (var entity in domainEventEntities)
        {
            var events = entity.Events.ToArray();
            entity.Events.Clear();
            foreach (var domainEvent in events)
            {
                _dispatcher.Dispatch(domainEvent);
            }
        }
    }
    while (ChangeTracker.Entries<IDomainEntity>().Any(po => po.Entity.Events.Any()));

    return base.SaveChanges();
}
like image 358
Korijn Avatar asked Oct 31 '14 15:10

Korijn


1 Answers

Yeah, you misunderstood things. A Domain Event is not like a C# event, it's a message about what changed in the Domain. One rule is that an event is something that happened, it's in the past. Thus, an event handler simply can't (it shouldn't) change the event, it's like changing the past.

You CouponActivatedHandler should at least get the User entity form a repository then update it with the number of credits, then save it, then publish an UserCreditsAdded event. Even better, the handler should just create and send a command AddCreditsToUser .

With Domain Events pattern, an operation is simply a chain of command->event-> command-> event etc. The event handler is usually a service (or a method in one) which takes care only of that bit. The sender of the event won't know anything about the handler and vice-versa.

As a thumb rule, a Domain object generates an event when its state has changed. A service will take those events then send them to a service bus (for a simple app, a DI Container is enough) which will publish them to be handled by anyone interested (this means services from the local app or other apps subscribed to that bus).

Never forget that Domain Events is a high level pattern used when doing the architecture of an app, it's not just another way to do object events (like C#'s events).

like image 107
MikeSW Avatar answered Sep 26 '22 12:09

MikeSW