Domain logic in command handler or event handler? - domain-driven-design

I am using cqrs and ddd to build my application.
I have an account entity, a transaction entity and a transactionLine entity. A transaction contains multiple transactionLines. Each transactionLine has an amount and points to an account.
If a user adds a transactionLine in a transaction that already has a transactionLine that points to the same account as the one the new transactionLine, I want to simply add the new transactionLine amount to the existing one, preventing a transaction from having two transactionLines that point to the same account.
Ex :
Before command :
transaction
transactionLine1(amount=100, account=2)
transactionLine2(amount=50, account=1)
Command :
addNewTransaction(amount=25, account=1)
Desired result :
transaction
transactionLine1(amount=100, account=2)
transactionLine2(amount=75, account=1) // Add amount (50+25) instead of two different transactionLines
instead of
transaction
transactionLine1(amount=100, account=2)
transactionLine2(amount=50, account=1)
transactionLine3(amount=25, account=1) // Error, two different transactionLines point to the same account
But I wonder if it is best to handle this in the command or the event handler.
If this case is handled by the command handler
Before command :
transaction
transactionLine1(amount=100, account=2)
transactionLine2(amount=50, account=1)
Command :
addNewTransaction(amount=25, account=1) // Detects the case
Dispatches event
transactionLineAmountChanged(transactionLine=2, amount=75)
AddTransactionLine command is received
Check if a transactionLine exists in the new transactionLine's transaction with the same account
If so, emit a transactionAmountChangedEvt event
Otherwise, emit a transactionAddedEvt event
Corresponding event handler handles the right event
If this case is handled by the event handler
Before command :
transaction
transactionLine1(amount=100, account=2)
transactionLine2(amount=50, account=1)
Command :
addNewTransaction(amount=25, account=1)
Dispatches event
transactionLineAdded(transactionLine=3, amount=25)
Handler // Detects the case
transactionLine2.amount = 75
AddTransactionLine command is received
TransactionLineAdded event is dispatched
TransactionLineAdded is handled
Check if the added transaction's transactionLine points to the same account as an existing transactionLine in this account
If so, just add the amount of the new transactionLine to the existing transactionLine
Otherwise, add a new transactionLine

Neither commands nor events should contain domain logic, only the domain should contain the domain logic. In your domain, aggregate roots represent transaction boundaries (not your transaction entities, but transactions for the logic). Handling logic within commands or events would bypass those boundaries and make your system very brittle.
The right place for that logic is the transaction entity.
So the best way would be
AddTransactionCommand finds the correct transaction entity and calls
Transaction.AddLine(...), which does the logic and publishes events of what happened
TransactionLineAddedEvent or TransactionLineChangedEvent depending on what happened.

Think of commands and events as 'containers', 'dtos' of data that you are going to need in order to hydrate your AggregateRoots or send out to the world (event) for other Bounded Contexts to consume them. That's it. Any other operation that is strictly related to your Domain has no place but your AggregateRoots, Entities and Value Objects.
You can add some 'validation' to your Commands, either by using DataAnnotations or your own implementation of a validate method.
public interface ICommand
{
void Validate();
}
public class ChangeCustomerName : ICommand
{
public string Name {get;set;}
public void Validate()
{
if(Name == "No one")
{
throw new InvalidOperationException("Sorry Aria Stark... we need a name here!");
}
}
}

Related

Intercepting an assignment to a table relation field

I created a one-to one relationship between two tables in strapi.
As an example, suppose that Bob currently has a job, say messenger, if we assign Bob’s Job to secretary, Strapi simply reassigns the new Job, without warning that Bob was already in a job
If a person is not in a current job, it’s job would be ‘none’
I’d like to forbid the reassignment of the job, if Bob was already in a job (the user would have to assign the Bob's job to ‘none’ before assigning a new job)
In strapi, what would be the right way to forbid it (checking if the current job is not ‘none’, and, if it’s the case, stopping the assignment), using a service, a controller or a lifecycle hook?
One way to handle this in Strapi would be to use a lifecycle hook. Lifecycle hooks allow you to perform specific actions at certain stages of the CRUD operations (create, update, delete) on a model. In this case, you can use the beforeUpdate hook to check if the current job is not none before allowing the assignment of a new job:
// api/person/models/Person.js
module.exports = {
lifecycles: {
// This hook will be called before updating a person
async beforeUpdate(params, data) {
// Check if the current job is not 'none'
if (params.current.job !== 'none') {
// If the current job is not 'none', throw an error
throw new Error('Cannot reassign a job to a person who already has a job');
}
}
}
};
You can also use a service or a controller to handle this logic, but using a lifecycle hook allows you to centralize this logic and keep it separate from your business logic.

How to block a change of state for an order transaction?

I am dealing with a Shopware6 plugin for a payment gateway. I have to handle the change of the state for the order transaction.
In one of my case, I would like to prevent the change of state in Refunded for an order transaction when I am trying to perform a refund on a payment provider and I receive an error.
I found this method \Shopware\Core\System\StateMachine\StateMachineRegistry::transition
public function transition(Transition $transition, Context $context): StateMachineStateCollection
{
...
$repository->upsert($data, $context);
...
$this->eventDispatcher->dispatch(new StateMachineTransitionEvent(...));
...
$this->eventDispatcher->dispatch(...);
...
$this->eventDispatcher->dispatch(...);
...
}
where more than one event is dispatched, but all of them after the upsert.
Even if I raise an Exception in a subscriber of those events, the upsert already updated the db table and I have no chance to revert it, especially because the Refunded state is a no exit state.
How can I prevent or revert the upsert?
You can revert the upsert. For that you would need to write a new transition to the database.
The table you are looking for is state_machine_transition and how the data is written to that table can be found under platform/src/Core/Migration/Migration1536233560BasicData.php#1078. You will need to do this in your plugin migration.
Then you could transition the state again after it was changed to your edgecase.

Get Modified Fields in RowPersisted Event

In RowPersisted event. Is there a way to know which fields were updated ? I have a customization in my RowPersisted event. But I only want to execute it if certain field(s) were actually modified. At the moment the event is firing unnecessarily as it reacts every time its saved.
TIA
UPDATE
Just to add. My customization has got nothing to do with the field values nor overriding the saving itself. I'm just using the RowPersisted event to kick off my customization.
If you want to compare the current row with any changes to the row with unchanged values (as it was from last persist) you can use a cache instance and call GetOriginal.
For example using an extension on sales order to check to see if the order qty or order total changed...
[PXOverride]
public virtual void Persist(Action del)
{
// Current object with any changed values
var salesOrder = Base.Document.Current;
// Unchanged object as it was set from the last save/persist
var unchangedSalesOrder = Base.Document.Cache.GetOriginal(salesOrder);
if (!Base.Document.Cache.ObjectsEqual<SOOrder.orderQty, SOOrder.curyOrderTotal>(salesOrder, unchangedSalesOrder))
{
PXTrace.WriteInformation("My values changed");
}
del?.Invoke();
}
Edit: I think at some point the GetOriginal was not publicly accessible. Not sure which version but if you cannot find this call it could be you are on an older version of Acumatica where this call cannot be used.
Below is the description of the RowPersisted event from https://help-2018r2.acumatica.com
public delegate void PXRowPersisted(PXCache sender, PXRowPersistedEventArgs e )
Parameters
sender (Required). The cache object that raised the event
e (Required). The instance of the PXRowPersistedEventArgs type that holds data for the >RowPersisted event
The RowPersisted event is triggered in the process of committing changes to the database for every data record whose status is Inserted, Updated, or Deleted. The RowPersisted event is triggered twice:
When the data record has been committed to the database and the status of the transaction scope (indicated in the e.TranStatus field) is Open.
When the status of the transaction scope has changed to Completed, indicating successful committing, or Aborted, indicating that a database error has occurred and changes to the database have been dropped.
The e parameter has the only Row property which is the current modified record.
You can check your condition on the e.Row and execute your code.
You should not use PXRowPersisted event for modifying values on the Completed transaction. If you need to modify values before/after save the best practice is to override Persist and use PXTransactionScope and invocation of the baseMethod, see example below:
[PXOverride]
public void Persist(Action baseMethod)
{
using(PXTransactionScope sc = new PXTransactionScope())
{
//... do your code here
baseMethod?.Invoke();
//... or here
sc.Complete();
}
}
UPDATED
Ideally, you should follow the rules below:
if you want to update the values of other fields of your record during the update of some fields then you should use the corresponding PXFieldUpdated event handler.
If you want to prevent saving of the record depending on some conditions of the field values of your record you should you PXRowPersisting event handler.
If you want to update DAC/Table of other maintenance/entry you should do it in the Persist method.

Where to put outside-aggregate validation?

I've got a question regarding outside-aggregate validation.
In our domain partner can place orders that contain certain products (1).
Once order is placed (2) he can mark it as paid (3) in our system.
Once order is marked as paid (4) we assign licences to products in external library service (5).
Once we know licences are assigned (6) we close entire saga.
Here's a small drawing illustrating the process:
At this moment besides commands, command handlers and events there are two domain classes that are involved in entire process:
Order aggregate containing business logic
Order saga coordinating entire process and assigning licences
Now, there is one invariant that is not modelled in this process yet - before we mark order as paid we have to check if user does not already have particular licence assigned. We get this from library service as well.
Where would you put this validation? Command handler? Wrap Order in some domain service? Pass some validator to Order constructor?
class Order
{
public function __construct(OrderValidator $validator)
{
if (!$validator->isValid($this)) {
throw new \DomainException();
}
// else proceed
}
}
class OrderValidator
{
private $libraryServiceClient;
public function isValid(Order $order)
{
// check licence using $libraryServiceClient
}
}
As far as I understood the problem is in step 3 (Mark order as payed). In this step we need a user (let's call it payer) that marks the order as payed. So when creating this payer object (using factory maybe) we need to know if he is allowed to mark an order as payed. In order to get this information a call should be made to the external library.
What I suggest is to have an application service that have ->markOrderAsPayed($orderId, $payerUserId)
This method will make a call to 2 domain services. One for getting the payer and one for marking the order as payed.
$payer = $this->payerService->getPayer($payerUserId);
$this->orderService->payOrder($orderId, $payer);
In the getPayer() function you should make a call to the external library to know how many licences the payer have.
I hope this will be helpful, it is just based on what I understood from the questions and comments.

Optimistic concurrency despite lock

I have multiple threads running a batch job. When each thread finishes it calls this method of mine:
private static readonly Object lockVar = new Object();
public void UserIsDone(int batchId, int userId)
{
//Get the batch user
var batchUser = context.ScheduledUsersBatchUsers.SingleOrDefault(x => x.User.Id == userId && x.Batch.Id == batchId);
if (batchUser != null)
{
lock (lockVar)
{
context.ScheduledUsersBatchUsers.Remove(batchUser);
context.SaveChanges();
//Try to get the batch with the assumption it has no users left. If we do get the batch back, it means there are no users left.
var dbBatch = context.ScheduledUsersBatches.SingleOrDefault(x => x.Id == batchId && !x.Users.Any());
//So this must have been the last user, the batch is empty, so we fetch it and remove it.
if (dbBatch != null)
{
context.ScheduledUsersBatches.Remove(dbBatch);
context.SaveChanges();
}
}
}
}
What this method does is very simple, it looks up the "BatchUser" to remove him from the queue, which it does. That part works swell.
However, after removing the user I want to check if that was the last user in the whole batch. But since this is multithreaded a race condition can happen.
So I put the removing of the batch user within a lock, after I remove the user, I check if the batch has no more batch users.
But here is my problem... even tho I have a lock, and the query to get the "dbBatch" clearly requires it to have no users to return the object... even so, I sometimes get it back with users like so:
When I do get that, I also get the following error on SaveChanges()
However, at other times I get the dbBatch object back correctly with no children, like so:
And when I do, it all works great, no exceptions.
With debugger I can catch the error by setting a breakpoint on the lock statement (see screenshot one). Then all threads get to the lock (while one goes in). Then I always get the error.
If I only have a breakpoint inside the if-statement it's more random.
With the lock in place, I don't see how this happens.
Update
I Ninject my context, and this is my ninject code
kernel.Bind<MyContext>()
.To<MyContext>()
.InRequestScope()
.WithConstructorArgument("connectionStringOrName", "MyConnection");
kernel.Bind<DbContext>().ToMethod(context => kernel.Get<MyContext>()).InRequestScope();
Update 2
I also tried this solution https://msdn.microsoft.com/en-us/data/jj592904.aspx
But strangely I don't get a DbUpdateConcurrencyException but rather I get a DbUpdateException that has an InnerException that is OptimisticConcurrencyException.
But neither DbUpdateException or OptimisticConcurrencyException contains a Entries property so I can't do ex.Entries.Single().Reload();
I'm also adding the exception in text form here
Here in text also, The outer exception of type DbUpdateException: {"An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details."}
The InnerException of type OptimisticConcurrencyException: {"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions."}

Resources