I design my aggregates to enforce invariants and business rules. I always consider my aggregate as a "write model" / "transaction model".
Considering that, I always feel free to expose "complex" GET methods in my repositories, using custom SQL / Linq, because I am not altrering data.
For example, consider two fictive aggregate roots (Student and School), using only reference ids between them. If I want to send an email to all students of a school, I'd typically use a domain service that executes this, for performance purpose:
- Calls a repository with a method like "GetAllStudentEmailAddressesForSchool(Guid schoolId)"
- Sends an email for each student
Of course, the repository implementation would required to do a "complex" query (JOIN) on the two aggregates.
The typical alternative would be to orcherstrate multiple calls on repositories in a domain service:
- Fetch the school from the ISchoolRepository
- Foreach StudentId of the School, call the IStudentRepository
(Of course it depends of the aggregate modelization, but the basic concept remains).
I had a lot debates with colleagues about this, many argue that the first approach leaks domain business outside the domain (aka in the infrastructure layer, in the repository implementation).
For me, it is a simple filtering strategy, the same way a ISchoolRepository would expose a method like "GetSchoolsFromCity(Guid cityId)". The fact that the filtering occurs on a single aggregate instead of two changes nothing.
Any opinions about that?