Some texts I've read regarding DDD indicate that an Application Service or a Command (CQRS) in the Application Layer closely mirrors a specific Use Case.
For simple use cases, this mapping makes sense, but in more complicated instances that require multiple user interactions, I'm trying to understand how to map the courser grained level of the API, without pushing application logic into the UI.
Example: - Imagine a Application Service:
ImportProductData(date_source)
My usual approach: Expand the API to include:
DoesIncludeExistingProducts(data_source)
If returns true, Prompt the user whether they would like to proceed, then call.
ImportProductData(date_source, overwrite=True)
My question is whether this is moving to much of the Application Logic into the UI layer? (i.e. the UI is now controlling whether Products can be overwritten, and whether existing products should be checked before importing product data etc)
If it is, I can't picture how the Application and Domain Layers would handle this? Apart from:
Calling:
ImportProductData(date_source)
When if fails, check why it failed, and if due to a product already existing, prompt the user and call again with:
ImportProductData(date_source, overwrite=True)
This feels like a different way of doing the same thing as above.
This might seem a bit pedantic, but I'm trying to make an concerted effort to keep the Presentation Layer (MVC) as light and thin as possible.
Thoughts as to any more elegant solutions?
First check out Jimmy Bogards enlightening post here https://jimmybogard.com/domain-command-patterns-validation/. He doesn’t give an answer to your question, but attacks it in a way that can make it easier for you to make up your mind.
In that article he briefly touches on a point that I believe needs more attention. He recommends not using exceptions as a communication mechanism if “the user frequently tries to do something that my domain validation disallows”. Using this criteria I prefer to break up use cases that are expected to sometimes fail into a validation call followed by an execution call.
In your case it seems appropriate to have a validation query (not a command) such as IsProductSetValidForImport. In Jimmy’s article above, he struggles with deciding how rich the returned set of errors should be, but the return set from a query is supposed to be rich so we don’t have a problem. You can return a real, fully formed view model to display to the user instead of trying to cram enough data into an error string to paint the screen. I assume if 3 out of 10 products fail the import you may want to allow the user to force update on some and not others. This gives you that opportunity.
If this validation query returns no conflicts, give the user an “are you sure” message, then call the API to execute the import command. If some significant state changed sneaked in between the time you called the validation query and the time you issued the update command, then it’s appropriate to throw an exception and handle the repercussions.