0

I have set my Domain Model objects to be independent of any service and infrastructure logic. I am also using Domain Events to react to some changes in Domain Models.

Now my problem is how to raise those events from the Domain Model objects itself. Currently I am using Udi Dahan's DomainEvents static class for this (I need evens to be handled exactly when they happen and not at a latter time). The events are used for many things, like logging, updating the data in related services and other Domain Model objects and db, publishing messages to the MassTransit bus etc.

The DomainEvents static class uses Autofac scope that I inject at some point in it, to find the IMediatr instance and to publish the events, like this:

public static class DomainEvents
{
    private static ILifetimeScope Scope;

    public async static Task RaiseAsync<TDomainEvent>(TDomainEvent @event) where TDomainEvent : IDomainEvent
    {
        var mediator = Scope?.Resolve<IMediatorBus>();
        if (mediator != null)
        {
            await mediator!.Publish(@event).ConfigureAwait(false);
        }
        else
        {
            Debug.WriteLine("Mediator not set for DomainEvents!");
        }
    }

    public static void SetScope(ILifetimeScope scope)
    {
        Scope = scope;
    }
}

This all works ok in a single-threaded environment, but the method DomainEvents.SetScope() is a possible racing problem in multhi-threaded environment. Ie. When I introduce MassTransit and create message consumers, each Message consumer will set the current LifetimeScope to DomainEvents by that method, and here is the problem, each consumer will overwrite the lifetime scope with the new one.

Why I use DomainEvents static class? Because I don't want to pollute my Domain Model Objects with infrastructure stuff. I thought about making DomainEvents non static (define an interface), but then I need them injected in every Domain Model Object and I'm still thinking about this, but maybe there is a better way.

I want to know if there is a better way to handle this? Maybe some change in DomainEvents class? Or maybe remove the DomainEvents static class end use an interface or DomainService to do this. The problem is I don't like static classes, but I also don't like pushing non domain-specific dependencies into Domain Model Objects.

Please help.

UPDATE

To better clarify the process and for what I use DomainEvents... I have a long-running process that can take from few minutes to few hours/days to complete. So the process is going like this:

  1. I receive an message from MassTransit ie ProcessStartMessage(processId)
  2. Get the ProcessData for (processId) from Db.
  3. Construct an in-memory Domain Model ProcessTracker (singleton) and put all the data I loaded from DB in it. (in-memory cache)
  4. I receive another message from Masstransit ie. ProcessStatusChanged(processId, data).
  5. Forward this message data to in-memory singleton ProcessTracker to process.
  6. ProcessTracker process the data.

For ProcessTracker to be able to process this data it instantiates many Domain Model Objects, each responsible to process some part of the data. (Note there is NO more db calls and entity hydration from db, it all happens in memory, also Domain Model is not mapped to any entity, it is not connected to any db object). At some point I need to log what a Domain Model object in the chain has done, has it work finished or started, has reached some milestone etc. This is done by raising DomainEvents. I also need to notify the GUI of those events, so they are used to send Masstransit messages too.

Ie.(pseudo code):

public class ProcessTracker 
{
    private Step _currentStep;
    
    public void ProcessData(data)
    {
        _currentStep.ProcessData(data);
        DomainEvents.Raise(new ProcesTrackerDataProcessed());
        ...
    }
 }

 public class Step 
 {
    public Phase _currentPhase;
  
    public void ProcessData(data)
    {
        if (data.IsManual && _someOtherCondition())
        {
            DomainEvents.Raise(new StepDataEvent1());
            ...
        }

        if(data.CanTransition)
        {
            DomainEvents.Raise(new TransitionToNewPhase(this, data));
        }

        _currentPhase.DoSomeWork(data);
        DomainEvents.Raise(new StepDataProcessed(this, data));
        ...
    }
 }

About db updates, those are not transactional and not important to the process and the Domain Model Object state is kept only in memory, if the process crash the process MUST begin from the start (there is NO recovery).

To end the process:

  1. I receive ProcessEnd from the MassTransit
  2. The message data is forwarded to the ProcessTracker
  3. ProcessTracker handles the data an nets a result of the proceess
  4. The result of the process is saved to db
  5. A message is sent to other parties in the process that notifies them of a process completion.
Luka
  • 4,075
  • 3
  • 35
  • 61

1 Answers1

0

Ask yourself first what are you going to do when you raise an event from your domain model?

Normally it works like this:

  • Get a command
  • Load a domain object from a repository
  • Execute behaviour
  • (here probably) Raise an event
  • Persist the new domain object state

So, where your extra domain event handlers would fit? Are you going to execute some other database calls, send an email? Remember that it all happens now, when you haven't even persisted the changed state of your domain object. What if your persistence fails? It will happen after you executed all the domain handlers.

You should not execute more than one transaction when you handle a single command. The Aggregate pattern clearly tells you that the aggregate is the transaction boundary. You should raise domain events after you complete the transaction, or within the same technical transaction, but it should only persist the aggregate state and the event. Domain event reactions potentially trigger transactions for other domain objects, and it should be done outside of the scope of handling the current command.

The issue is not at all technical, it is a design problem.

If you use MassTransit, you can only make it (relatively) reliable if you handle the command in a message consumer. Then, you can use in-memory outbox, which will not send an event unless the consumer succeeded. It is still not guaranteed that the event will be published in case of the broker failure.

Unless you go to Event Sourcing, you have two 100% reliable options:

  • Use a transactional outbox pattern (NServiceBus has one and it's quite complex). It has limitations on what type of database you use.
  • Store the event to the same database as the domain object, in a different table, within the same transaction. Poll the table with DELETE INTO and emit events to the broker from there.
Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83
  • I have updated the question with more info (see UPDATE) – Luka Mar 25 '21 at 11:10
  • I let aggregate root return list of events and then raise them from mediatr handler. Why not inject eventSender into mediatr handler? It doesn't have to be static. – Christian Johansen Mar 29 '21 at 15:55
  • @Luka you are deliberately creating issues for your future self. just think about running this application in two instances and then try to figure out if it will work. – Alexey Zimarev Mar 29 '21 at 21:47
  • @ChristianJohansen it's not MediatR. It is a Mediator feature of MassTransit. Anyway, handling domain events in-proc, with additional side effects (not part of handling a command) is a straight road to countless issues. I am not sure what's the point in all that. – Alexey Zimarev Mar 29 '21 at 21:50
  • Ha thx for pointing that out @alexey my Bad. The eventSender could be sending to an out of process transport from a commandHandler, but i guess its irrelevant for the question. – Christian Johansen Mar 30 '21 at 06:38
  • @ChristianJohansen As I wrote, I need them executed exactly when they happen not at the finish of an request/transaction. About injecting eventSender into domain events - this is the question - is this a good solution? Because I am injecting an infrastructure specific logic into my domain objects. Is this ok? – Luka Mar 31 '21 at 06:40
  • @AlexeyZimarev I am not talking about MassTransit mediator, but the MediatR library itself. The application is a windows service that MUST/WILL be hosted on a single machine only. I know my solution is problematic, this is why I have asked the question. The question is, How to properly publish the domain events from inside the domain object itself without using the static class, is it good practice to inject infrastructure class for sending events into domain objects? – Luka Mar 31 '21 at 06:43