Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Question: SaveChanges() and side effects of a domain event within a logical transaction #721

Closed
ig16022 opened this issue Aug 25, 2018 · 6 comments

Comments

@ig16022
Copy link

ig16022 commented Aug 25, 2018

Even though there is an 'await' on
await _mediator.DispatchDomainEventsAsync(this);

within the Ordering context's SaveEntitiesAsync() method and the base.SaveChanges() will clearly wait for the domain event handlers to complete their work, I also see

await _buyerRepository.UnitOfWork
.SaveEntitiesAsync();

in the domain event handler ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler (Handle() method), and it appears that the buyer will be saved immediately, not as part of a transaction with the Order. This goes against the idea of including Side Effects in an Aggregate's 'save' transaction. Am I not seeing something here?

Additionally, if the domain event handler was creating a new buyer, and the buyer's Id was needed to save an Order, how can this be done with this pattern?

@mvelosop
Copy link
Collaborator

Hi @ig16022,

It looks like you are right.

We were recently alerted about a related issue (#700) and it looks like there's bug regarding transaction atomicity in domain and integration event handling.

One of the possible solutions could be:

Before saving changes check if there's a transaction and if there's none, create one and only commit the transaction if you (the command handler perhaps) created it. This should handle "cascading" domain events.

Some other change could be handling an in-memory list of integration events, similar to the current domain events list on root aggregates, and committing them all at once within a transaction scope similar to the one mentioned.

Work on this issue is just beginning.

Best.

cc: @unaizorrilla

@ig16022
Copy link
Author

ig16022 commented Aug 28, 2018

This helps, thank you.

@arielmoraes
Copy link

arielmoraes commented Sep 1, 2018

@mvelosop I've modified the code from our application by following your tips. Now it looks like the following:

public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
	var alreadyInTransaction = Database.CurrentTransaction != null;
	if (!alreadyInTransaction)
	{
		await Database.BeginTransactionAsync();
	}

	await _mediator.DispatchDomainEventsAsync(this);

	if (!alreadyInTransaction)
	{
		base.SaveChanges();
		Database.CurrentTransaction.Commit();
	}
}

Any tips if it can be improved even further? Now I need only to create some kind of queue to achieve idempotency between all of this and the Integration Events.

I choose to change the code inside the context because I prefer not to have the commands do the logic if they should have to SaveChanges or not.

@mvelosop
Copy link
Collaborator

Hi @arielmoraes,

That's kind of the general idea.

One comment here is that you can save on your DbContext when inside a transaction, as updates will only change the DB when committing the transaction.

Thinking a bit about this while on a short vacation, I got a (rough) idea of extending IUnitOfWork to have a BeginTransaction and a CommitTransaction, so the command handlers would be the ones that try beginning the transaction and, if succeeded, they would then have to commit it.

BeginTransaction would hide a lot of the details you have there and enforce the protocol, as only the command handler that got the transaction would be able to commit it.

In the case of cascading command handlers being invoked, they wouldn't be able to begin another transaction or commit the current one so all changes would fall under the only one transaction.

As for the queue for integration events, you should be able to do without it, because, you would be inside a transaction so any changes you save to your DbContext would not be committed to the DB up until the transaction commit.

WORD OF CAUTION This is an untested rough idea and there are probably a lot of details there to solve but I think the solution could be in that direction.

There's some special handling that would have to be done here if implementing resilient transactions but that would be for v2 😉.

@mvelosop
Copy link
Collaborator

mvelosop commented Feb 8, 2019

Hi @ig16022, @arielmoraes,

This issue was solved, along with #828, with the following approach:

  1. Begin a transaction in the TransactionBehaviour:

  2. While processing the related commands, add the resulting integration events to a list (an outbox):

    await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);

    There's a nice detail here, and it's that the outbox is persisted in the DB, so it'd be easy to implement a "watchdog" microservice that would handle possible failures in the publishing mechanism. This is not implemented in eShopOnContainers.

  3. Commit the transaction for all changes when returning from the behavior pipeline:

  4. Publish all integration events in the outbox:

    await _orderingIntegrationEventService.PublishEventsThroughEventBusAsync();

So this should handle this situation and the one from issue #828.

Any comments on this?

@mvelosop
Copy link
Collaborator

mvelosop commented Feb 8, 2019

This issue is solved now, so I'm closing it, but feel free to comment, will reopen if needed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants