In many cases concurrency errors can be easily resolved without having to return the client an error.
Using a customer as an example, changing a customers name shouldn't conflict with an delivery address change, in this case you should be able to simply rebase the name change after the address change.
This behaviour would most-often be defined by the business requirements and processes so allowing consumers to optionally provide/consume these in CommandHandlers / Saving an aggregate would be preferable.
By default, the implementations should simply noop and rethrow the error I suspect, and once additional implementations are supplied use those to attempt conflict resolution.
My initial thoughts are:
public interface IConcurrencyConflictResolver<TAggregate>
{
Task /* What should we return here, if anything? */ ResolveAsync(TAggregate aggregate, IEnumerable<IEvent> conflictingEvents);
}
public MyCommandHandler : ICommandHandler<TCommand>
{
public MyCommandHandler(/* ... */, IConcurrencyConflictResolver<MyAggregate> conflictResolver)
{
/* ... */
}
}
This is subject to change as I think this through and get feedback from others.
In many cases concurrency errors can be easily resolved without having to return the client an error.
Using a customer as an example, changing a customers name shouldn't conflict with an delivery address change, in this case you should be able to simply rebase the name change after the address change.
This behaviour would most-often be defined by the business requirements and processes so allowing consumers to optionally provide/consume these in CommandHandlers / Saving an aggregate would be preferable.
By default, the implementations should simply noop and rethrow the error I suspect, and once additional implementations are supplied use those to attempt conflict resolution.
My initial thoughts are:
This is subject to change as I think this through and get feedback from others.