If you want to add a method to a class hierarchy without actually adding the method, consider the Visitor Pattern. You could create a validation visitor, and let each entity select the appropriate method of the visitor.
First, your ParentEntity class hierarchy would need a bit of boilerplate to support visitors:
interface EntityVisitor<T> {
T visitA(AEntity a);
T visitB(BEntity b);
}
class ParentEntity {
<T> T accept(EntityVisitor<T> v);
}
class EntityA extends ParenEntity {
...
@Override <T> T accept(EntityVisitor<T> v) {
return v.visitA(this);
}
}
Next, we can implement and use a visitor that performs validation.
class Validation implements EntityVisitor<Void> {
EntityRepository repository;
...
@Override Void visitA(AEntity a) { ... }
@Override Void visitB(BEntity b) { ... }
}
class EntityRepository ... {
void save(List<ParentEntity> list) {
list.ForEach(e -> {
e.accept(new Validation(this));
...
});
}
}
The validation visitor can have access to both the entity and the repository (in order to make further queries), and will therefore be able to perform the full validation.
Using such a pattern has advantages and disadvantages compared to an instanceof check and compared to moving the validation logic into the entities.
An instanceof is a much simpler solution, especially if you only have very few entity types. However, this could silently fail if you add a new entity type. In contrast, the visitor pattern will fail to compile until the accept() method is implemented in the new entity. This safety can be valuable.
While this pattern ends up having the same behaviour as adding a validate() method to the entities, an important difference is where that behaviour is located and how our dependency graph looks. With a validate() method, we would have a dependency from the entities to the repository, and would have referential integrity checks intermixed with actual business logic. This defeats the point of an Onion Architecture. The visitor pattern lets us break this dependency and lets us keep the validation logic separate from other business logic. The cost of this clearer design structure is extra boilerplate in the form of the EntityVisitor interface and the accept() method that must be added to all entities in the relevant class hierarchy.
Whether these trade-offs are worth it is your call. You know your codebase best, and you have the best idea how it might evolve.
However, performing validation based on the result of multiple queries can lead to data integrity problems. The repository should either make sure to use database transactions (and offer an API that clearly communicates when modifications have been committed), or the relevant integrity checks should be done within the database, e.g. using constraints in an SQL database. In some cases, the validation checks can also be expressed as part of an insert or update query.