Where to raise persistence-dependent domain events - service, repository, or UI?
Asked Answered
P

6

35

My ASP.NET MVC3 / NHibernate application has a requirement to fire off and handle a variety of events related to my domain objects. For example, an Order object might have events like OrderStatusChanged or NoteCreatedForOrder. In most cases these events result in an email being sent, so I can't just leave them in the MVC app.

I've read through Udi Dahan's Domain Events and dozens of other thoughts on how to do this sort of thing, and I decided on using an NServiceBus-based host that handles event messages. I've done a few proof-of-concept tests and this seems to work well.

My question is what application layer should actually raise the events. I don't want to fire the events until the object in question has been successfully persisted (can't send an email that a note was created if the persistence failed).

Another concern is that in some cases an event is tied to an object which is beneath an aggregate root. In the example above, a Note is saved by adding it to the Order.Notes collection and saving the order. This poses a problem in that it makes it tough to evaluate what events should get fired when an Order is saved. I'd like to avoid having to pull a current copy of the object and look for differences prior to saving the updated copy.

  • Is it appropriate for the UI to raise these events? It knows what events have occurred and can fire them only after successfully having the service layer save the object. Something just seems wrong about having a controller firing off domain events.

  • Should the repository fire off events after successfully persisting?

  • Should I separate the events altogether, have the repository store an Event object which is then picked up by a poller service and then turned into an event for NServiceBus (or handled directly from the poller service)?

  • Is there a better way to do this? Maybe having my domain objects queue up events which are fired by the service layer only after the object is persisted?

  • Update: I have a service layer, but it seems cumbersome and excessive to have it go through a comparison process to determine what events should be fired when a given aggregate root is saved. Because some of these events are granular (e.g. "order status changed"), I think I'd have to retrieve a DB copy of the object, compare the properties to create events, save the new object, then send the events to NServiceBus when the save operation completed successfully.

Update

What I ended up doing, subsequent to the answer I posted below (way below), was to build into my domain entities an EventQueue property that was a List<IDomainEvent>. I then added events as changes to the domain merited it, which allowed me to keep the logic within the domain, which I believe is appropriate since I'm firing events based on what's going on within an entity.

Then, when I persist the object in my service layer, I process that queue and actually send the events to the service bus. Initially, I was planning on using a legacy database that used identity PKs, so I had to post-process these events to populate the ID of the entity, but I ultimately decided to switch to a Guid.Comb PK which allows me to skip that step.

Panegyric answered 4/5, 2011 at 15:46 Comment(1)
Take a look at thisPterous
P
1

The solution turned out to be based on implementing these extension methods on the NHibernate session object.

I was probably a little unclear with the wording of the question. The whole reason for the architectural issue was the fact that NHibernate objects are always in the same state unless you manually un-proxy them and go through all sorts of machinations. That was something I didn't want to have to do to determine what properties had been changed and therefore what events to fire.

Firing these events in the property setters wouldn't work because the events should only fire after changes have been persisted, to avoid firing events on an operation that might ultimately fail.

So what I did was add a few methods to my repository base:

public bool IsDirtyEntity(T entity)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}

public bool IsDirtyEntityProperty(T entity, string propertyName)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}

Then, in my service's Save method, I can do something like this (remember I'm using NServiceBus here, but if you were using Udi Dahan's domain events static class it would work similarly):

var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
    pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object

_repository.Save(order);
_unitOfWork.Commit();

// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
    Bus.Send(message);

Because in some cases the Id of the entity might not be set until it's saved (I'm using Identity integer columns), I might have to run through after committing the transaction to retrieve the Id to populate properties in my event objects. Since this is existing data I can't easily switch to a hilo type of client-assigned Id.

Panegyric answered 6/5, 2011 at 13:31 Comment(2)
The transaction problem is very common with event based systems, and the solution to this problem is commmon too, and it doesn't care about Nhibernate, or any technology, it's just logical solutuion.Dric
If you're going to downvote an answer (or three), include some justification in a comment.Panegyric
W
10

My solution is that you raise events in both Domain layer and service layer.

Your domain:

public class Order
{
    public void ChangeStatus(OrderStatus status)
    {
        // change status
        this.Status = status;
        DomainEvent.Raise(new OrderStatusChanged { OrderId = Id, Status = status });
    }

    public void AddNote(string note)
    {
        // add note
        this.Notes.Add(note)
        DomainEvent.Raise(new NoteCreatedForOrder { OrderId = Id, Note = note });
    }
}

Your service:

public class OrderService
{
    public void SubmitOrder(int orderId, OrderStatus status, string note)
    {
        OrderStatusChanged orderStatusChanged = null;
        NoteCreatedForOrder noteCreatedForOrder = null;

        DomainEvent.Register<OrderStatusChanged>(x => orderStatusChanged = x);
        DomainEvent.Register<NoteCreatedForOrder>(x => noteCreatedForOrder = x);

        using (var uow = UnitOfWork.Start())
        {
            var order = orderRepository.Load(orderId);
            order.ChangeStatus(status);
            order.AddNote(note);
            uow.Commit(); // commit to persist order
        }

        if (orderStatusChanged != null)
        {
            // something like this
            serviceBus.Publish(orderStatusChanged);
        }

        if (noteCreatedForOrder!= null)
        {
            // something like this
            serviceBus.Publish(noteCreatedForOrder);
        }
    }
}
Wivinah answered 12/12, 2011 at 4:50 Comment(0)
P
7

Domain Events should be raised in... the Domain. That's why they are Domain Events.

public void ExecuteCommand(MakeCustomerGoldCommand command)
{
    if (ValidateCommand(command) == ValidationResult.OK)
    {
        Customer item = CustomerRepository.GetById(command.CustomerId);
        item.Status = CustomerStatus.Gold;
        CustomerRepository.Update(item);
    }
}

(and then in the Customer class, the following):

public CustomerStatus Status
{
    ....
    set
    {
        if (_status != value)
        {
            _status = value;
            switch(_status)
            {
                case CustomerStatus.Gold:
                    DomainEvents.Raise(CustomerIsMadeGold(this));
                    break;
                ...
            }
        }
    }

The Raise method will store the event in the Event Store. It may also execute locally-registered event handlers.

Prober answered 5/5, 2011 at 12:44 Comment(7)
That was where I started, but suppose there was an error during CustomerRepository.Update(item) and persistence failed. I wouldn't want that event to be handled then. That's why I'm wondering what the other options are. Doing it in the repository is a bad idea, as is doing it in the UI. Doing it in a service would be great if it didn't require that I hack around NH's tendency to ensure that two instances of the "same" object are identical, which prevents me from doing a comparison of the pre-change and post-change object properties in the service.Panegyric
Who has been downvoting so many answers here without any justification??Prober
@Josh, and Roy, I have downvoted this answer because as Josh correctly wrote in comment, your code is not transactional, if CustomerRepository.Update(item) will fail the event was raised already and actions were done, that shouldn't happen.Dric
Actually, there is some merit in Roy's suggestion. If the events are domain events, it isn't unreasonable to consider the entity keeping track of the events and then a service layer processing the events queued for the entity after persistence. I'm considering just this type of arrangement as an alternative and would be interested in hearing feedback on why this would or would not be a good idea.Panegyric
There is, but the software systems we build usually deal with domain Entities in transition state, that periodically sync with data store (get persisted, reloaded). And we need to take in account this. And you have 3 options to deal with this, inject persistance in Entity and have trash design, move event fire to higher level which known about persistance (service or repository), fire both events one in Entity - and do entity related things there that are persistance ignorant, the other in perstance aware layer, and do more technical things there.Dric
Fantastic remark, Alex Burtsev. The code is not transactional... And who said that DDD is by definition ACID? DDD is often used in combination with CQRS (where it really shines) and CQRS is not ACID. But thanks for clarifying.Prober
BTW, the storage of the event in the Event Store could be a part of the transaction; in that case, it would not be stored if the update of the customer record failed.Prober
Y
4

It sounds like you need a Service Layer. A service Layer is another abstraction that sits between your front end or controllers and your business layer or domain model. It's kind of like an API into your application. Your controllers will then only have access to your Service Layer.

It then becomes your service layer's responsibility to interact with your domain model

public Order GetOrderById(int id) {
  //...
  var order = orderRepository.get(id);
  //...
  return order;
}

public CreateOrder(Order order) {
  //...
  orderRepositroy.Add(order);
  if (orderRepository.Submitchanges()) {
    var email = emailManager.CreateNewOrderEmail(order);
    email.Send();
  }
  //...
}

It's common to end up with 'manager' objects such as OrderManager to interact with orders and the service layer to deal with POCOs.

Is it appropriate for the UI to raise these events? It knows what events have occurred and can fire them only after successfully having the service layer save the object. Something just seems wrong about having a controller firing off domain events.

No. You will end up with problems if new actions are added and a developer is unaware or forgets that an email should be fired off.

Should the repository fire off events after successfully persisting?

No. the repository's responsability is to provide an abstraction over data access and nothing else

Is there a better way to do this? Maybe having my domain objects queue up events which are fired by the service layer only after the object is persisted?

Yes, it sounds like this should be handled by your service layer.

Yahairayahata answered 4/5, 2011 at 16:16 Comment(3)
Yeah, I have a service layer that's doing the saving, and that was the initial candidate for raising the events. The problem I ran into was that the service layer would end up having to pull a copy of some objects prior to saving the modified version to compare the new one against the old one and then decide what events should be fired.Panegyric
Is there a particular problem, most ORM's will handle that for you? Are you encountering a performance problem in doing it that way?Yahairayahata
Not exactly a performance issue, but trying to avoid having two instances of one logical object (see first answer here: #735825). NH doesn't really do anything to impact what I need: get order, change status, save order, fire event. If the change of status doesn't create some meta-data about the change, I couldn't fire the event after saving unless I did a property-by-property comparison of the order to be saved and a new instance of the order as it existed when originally retrieved.Panegyric
P
1

The solution turned out to be based on implementing these extension methods on the NHibernate session object.

I was probably a little unclear with the wording of the question. The whole reason for the architectural issue was the fact that NHibernate objects are always in the same state unless you manually un-proxy them and go through all sorts of machinations. That was something I didn't want to have to do to determine what properties had been changed and therefore what events to fire.

Firing these events in the property setters wouldn't work because the events should only fire after changes have been persisted, to avoid firing events on an operation that might ultimately fail.

So what I did was add a few methods to my repository base:

public bool IsDirtyEntity(T entity)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}

public bool IsDirtyEntityProperty(T entity, string propertyName)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}

Then, in my service's Save method, I can do something like this (remember I'm using NServiceBus here, but if you were using Udi Dahan's domain events static class it would work similarly):

var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
    pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object

_repository.Save(order);
_unitOfWork.Commit();

// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
    Bus.Send(message);

Because in some cases the Id of the entity might not be set until it's saved (I'm using Identity integer columns), I might have to run through after committing the transaction to retrieve the Id to populate properties in my event objects. Since this is existing data I can't easily switch to a hilo type of client-assigned Id.

Panegyric answered 6/5, 2011 at 13:31 Comment(2)
The transaction problem is very common with event based systems, and the solution to this problem is commmon too, and it doesn't care about Nhibernate, or any technology, it's just logical solutuion.Dric
If you're going to downvote an answer (or three), include some justification in a comment.Panegyric
D
1

I think, you shouold have a Domain Service, as David Glenn said.

The problem I ran into was that the service layer would end up having to pull a copy of some objects prior to saving the modified version to compare the new one against the old one and then decide what events should be fired.

Your Domain Service should contains methods that clearly state what you want to do with your domain Entity, like: RegisterNewOrder, CreateNoteForOrder, ChangeOrderStatus etc.

public class OrderDomainService()
{
    public void ChangeOrderStatus(Order order, OrderStatus status)
    {
        try
        {
            order.ChangeStatus(status);
            using(IUnitOfWork unitOfWork = unitOfWorkFactory.Get())
            {
                IOrderRepository repository = unitOfWork.GetRepository<IOrderRepository>();
                repository.Save(order);
                unitOfWork.Commit();
            }
            DomainEvents.Publish<OrderStatusChnaged>(new OrderStatusChangedEvent(order, status));
        }

    }
}
Dric answered 11/12, 2011 at 10:18 Comment(6)
I think it's a little strong to suggest that a service layer should provide granular methods to change individual entity properties. That type of approach would result in a very anemic domain entity, and a service layer that is giant and a pain to maintain. I'd have to prevent entity properties from being updated any way other than through related service layer methods, which isn't an ideal solution at all.Panegyric
You wouldn't perchance be the guy downvoting the other answers, all of which were reasonable ways to look at the problem?Panegyric
@Josh Domain Service I wrote as example doesn't provide methods to change individual properties. Methods should corespond to business actions. This aproach will not result in anemic model, notice order.ChangeStatus(status) line of code, I delegate all the logic work to Domain Entity.Dric
@Josh I actually prefer Onion Architecture (tonysneed.files.wordpress.com/2011/10/onion-arch.jpg) to layer one. So I would call this Domain Service Layer, to not confuse it with Application Serivce Layer, or Web Service Layer.Dric
The issue is that I can't assume that the events I'll need to fire will correspond exclusively to "business actions." Under your suggested solution, I would be unable to trigger an event except through a service layer method. This isn't ideal for cases where one logical transaction triggers multiple events as it would require a persistence operation for each event-related business action (recall that my original question was related to persistence-dependent events).Panegyric
@Josh You can (and maybe should) have separate events in each layer. In data access, Domain Service, and Domain Entities. I'm not telling to fire event on evety action, just the one you consider important. But here you are stepping on the path of Event based system, which is great in some situations due to high extensability, low coupling, high cohesion, but these types of designs are harder to read, and figure out what happens when. I have little experience in WEB apps, but If I would build one I think I would avaid events, and shift more towards CQRS design, which is very explicit.Dric
A
0

Having domain events works well if you have Commands, sent to your service layer. It then can update entities according to a command, and raise corresponding domain events in a single transaction.

If you are moving entities themselves between UI and service layer, it becomes very hard (and sometimes even impossible) to determine what domain events have occured, since they are not made explicit, but hidden under state of the entities.

Abm answered 4/5, 2011 at 17:9 Comment(4)
Although it would make for a heavy service layer (to be expected I guess), the concept of events being triggered in the context of commands would work in most situations. The only time it would be difficult is when an entity is initially created with a state or states that merit the firing of an event. So maybe we save an order and don't set it to Submitted, then fire the OrderSubmitted event with a SubmitOrder() command in the service. But we might also simply save an order with Submitted = true...how would I reconcile that with commands?Panegyric
@Josh so you'd have CreateOrder command, with all the necessary data to create the order (not the order itself, it still hasn't been created at that point!). You can indicate in that command if order should be submitted at once upon creation, and trigger OrderSubmitted event if needed when handling that command.Abm
The tough thing there is that I'd be losing all the advantages of NHibernate, specifically the cascading persistence capabilities. Right now I can create or retrieve an order, add a few notes, add some order items, then do one save on the order itself and have NH persist all the subordinate objects. Incidentally, NH is part of the problem here, too - because of its awareness of session contexts, it's not so simple just to pull a DB copy of an object and compare it with the in-memory copy. NH will synchronize the two and you won't see any differences in property values.Panegyric
No, you wouldn't lose anything NH has to offer. But you need to move all your domain logic from UI to service layer, which of course can be quite a task.Abm

© 2022 - 2024 — McMap. All rights reserved.