I have a document in this format:
{
_id: ...,
myArray: [{other: stuff}, {other: stuff}, ...],
...
}
I want to find elements that match certain things, like the _id
or fields value from the sub-documents in myArray
.
I want to return the documents, but with a filtered MyArray
where only the matching sub-documents are present.
I tried to do a projection and include the matched elements like this:
_mongoContext.myDocument
.Find(x => x.id == id & x.myArray.Any(y => myList.Contains(t.other)))
.Project<myModel>(Builders<myModel>.Projection.Include("myArray.$"))
This, I think, should only return the first element that matched in myArray
instead of all documents, which is not what I want (I want all sub-documents that match the query to be present in the returned document).
And anyway it did not even work, I'm getting a positional projection does not match the query document
error. Maybe it's because I'm not using FindOne
?
In any case, how can I achieve what I'm looking for? (See question in bold)
Typically you need to use $filter in Aggregation Framework to filter nested array. However there's an easier way to achieve that using MongoDB .NET Driver and IQueryable interface.
Considering simplest model:
public class MyModel
{
public string _id { get; set; }
public IEnumerable<MyNestedModel> myArray { get; set; }
}
public class MyNestedModel
{
public string other { get; set; }
}
and following data:
var m = new MyModel()
{
_id = "1",
myArray = new List<MyNestedModel>() {
new MyNestedModel() { other = "stuff" },
new MyNestedModel() { other = "stuff" },
new MyNestedModel() { other = "stuff2" } }
};
Col.InsertOne(m);
you can simply call .AsQueryable()
on your collection and then you can write LINQ query which will be translated by MongoDB driver to $filter
, try:
var query = from doc in Col.AsQueryable()
where doc._id == "1"
select new MyModel()
{
_id = doc._id,
myArray = doc.myArray.Where(x => x.other == "stuff")
};
var result = query.ToList();
EDIT:
Alternatively you can write $filter
part as a raw string and then use .Aggregate()
method. Using this approach you don't have to "map" all properties however the drawback is that you're losing type safety since this is just a string, try:
var addFields = BsonDocument.Parse("{ \"$addFields\": { myArray: { $filter: { input: \"$myArray\", as: \"m\", cond: { $eq: [ \"$$m.other\", \"stuff\" ] } } } } }");
var query = Col.Aggregate()
.Match(x => x._id == "1")
.AppendStage<MyModel>(addFields);
$addFields
is used here to overwrite existing field.