I am implementing a CQRS pattern in ASP.NET Core with MediatR, and I am wondering what the general consensus is for handling duplicate validation logic for queries/commands which operate on the same underlying model/entity.
For more context, I am looking through Jason Taylor's clean architecture project here as a general reference. In his project, he has a CreateTodoListCommand, and an UpdateTodoListCommand. Both operate on the same underlying TodoList
entity.
For validation, he has two validation classes: CreateTodoListCommandValidator, and UpdateTodoListCommandValidator. Both essentially perform the exact same validation. The code to perform this validation is duplicated. This seems like bad design to me, because, if there is a change to the validation in one place, you must remember to update validation in all places. In the two validation example from above, if the create command validator changed its length requirement from 200 to 100 characters, then I would need to remember to change the update command validator to max out at 100 characters as well. And this is a pretty trivial example. As the application grows, I feel this could easily grow way out of hand, and become extremely hard to maintain consistent validation across underlying entities.
Do people generally suck it up, and do their best to maintain the duplicate validation code?
One possible solution I have thought of is removing the ValidationBehaviour
from the MediatR pipeline, and injecting a general purpose validator service to each handler instead. The handlers would map the query/command to the underlying entity (which is has to do anyway), and then the entity would be validated with a general purpose validation service. Each handler could pass in the field names that it wants validated for its specific case if needed.
Thoughts on this approach? Or any other solutions that others have come up with?
FluentValidation
finds failures it will produce a dictionary to map field name to validation results. Whereas the business level validation that I have seen involves throwing exceptions which can't really be mapped to a field that it came from.