It seems likely that you're actually looking at a different bounded context here. You mentioned in your question that "repository ... can only pull aggregate roots."; this is correct. Another answer also mentions that if you need to query a child object, the child object may also be an aggregate root. This may also be correct within a different bounded context. It's quite possible for an entity to be an aggregate root in one context, and a value entity in another.
Take for example the domain of Users
and the mobile/tablet Apps
they have installed on their devices. In the context of the user, we might want the users basic properties such as name, age etc, and we might also want a list of apps the user has installed on their device. In this context User
is the aggregate root and App
is a value object.
bounded context UserApps
{
aggregate root User
{
Id : Guid
Name : string
Age : int
InstalledApps : App list
}
value object App
{
Id : Guid
Name : string
Publisher : string
Category : enum
}
}
In another context we may take an App
centric view of the world and decide that App
is the aggregate root. Say for example we wanted to report which users have installed a given app.
bounded context AppUsers
{
aggregate root App
{
Id : Guid
Name : string
InstalledBy : User list
}
value object User
{
Id : Guid
Name : string
InstalledOn : Date
}
}
Both of these bounded contexts would have their own repository which returns the respective aggregate root. There's a subtle but crucial difference in your perspective of the data.
I think if you take a step back and think about why you want to query for a child object, you might find that you're actually in an entirely separate bounded context.