2

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)

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Nice answer, the only downside is that I then need to set all fields in the MyModel manually in the select, one by one, and update this if the model change, right?
@DavidG. that's true, I've extended my answer and added a version with raw string approach, you can choose if that makes more sense in your case

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.