Uploaded image for project: 'C# Driver'
  1. C# Driver
  2. CSHARP-2723

Select() inside Project() does not properly reference the Project lambda variable

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Major - P3 Major - P3
    • 2.14.0
    • Affects Version/s: None
    • Component/s: Linq
    • Labels:
      None

      When a Select() is used inside a Project(), and the Select() expression refers to the Project() lambda variable, the generated projection does not properly reference the Project() lambda variable and its properties.

      Given a local database named "test" and

      db.parents.insert([
          {_id:1, name:"parent1"},
          {_id:2, name:"parent2"}
      ]);
      
      db.children.insert([
          {_id:1, name:"child1", parentId:2},
          {_id:2, name:"child2", parentId:1}
      ]);
      
      
      db.grandChildren.insert([
          {_id:1, name:"grandchild1", childId: 2},
          {_id:2, name:"grandchild2", childId: 1}
      ]);
      

      And query method GetTree()

      using System.Collections;
      using System.Collections.Generic;
      using System.Linq;
      using System.Threading.Tasks;
      using MongoDB.Bson;
      using MongoDB.Driver;
      
      namespace MongoDbTest.Repository
      {
          public class Repository : IRepository
          {
              private readonly IMongoCollection<Child> _children;
              private readonly IMongoCollection<GrandChild> _grandChildren;
              private readonly IMongoCollection<Parent> _parents;
      
              // There is no way to refer to a let variable ($$products) from a lambda function.
              // We have to use a BsonDocument (strings unfortunately) to define our lookup pipeline.
              // In reality, there are other criteria in the $and
              private const string DeliveriesLookupPipelineString = @"
      {$match:{
        $expr:{
          $and:[
            {$in:[""$childId"",""$$children._id""]}
           ]
        }
       }
      }
      ";
      
              private static readonly PipelineDefinition<GrandChild, GrandChild> GrandchildrenLookupPipeline = new[]
              {
                  BsonDocument.Parse(DeliveriesLookupPipelineString)
              };
      
              public Repository(
                  IMongoCollection<Parent> parents,
                  IMongoCollection<Child> children,
                  IMongoCollection<GrandChild> grandChildren)
              {
                  _parents = parents;
                  _children = children;
                  _grandChildren = grandChildren;
              }
      
              public Task<List<ParentTree>> GetTree()
              {
                  return _parents
                      .Aggregate()
                      .Lookup<Parent, Child, ParentProjection>(
                          _children,
                          parent => parent.Id,
                          child => child.ParentId,
                          parentProjection => parentProjection.Children)
                      .Lookup<ParentProjection, GrandChild, GrandChild, IEnumerable<GrandChild>, ParentProjection>(
                          _grandChildren,
                          new BsonDocument {{"children", "$children"}},
                          GrandchildrenLookupPipeline,
                          childProjection => childProjection.GrandChildren)
                      .Project(parent => new ParentTree
                      {
                          Id = parent.Id,
                          ParentName = parent.Name,
                          Children = parent.Children
                              .Select(child => new ChildTree
                              {
                                  Id = child.Id,
                                  Name = child.Name,
                                  GrandChildren = parent
                                      .GrandChildren // For some reason the generated code refers to child.grandChildren
                                      .Where(gc =>
                                          gc.ChildId ==
                                          child.Id) // and here the generated code compares gc.childId to gc._id rather than to child._id
                                      .Select(gc =>
                                          new GrandChildProjection()
                                          {
                                              Id = gc.Id,
                                              Name = gc.Name
                                          })
                              })
                      }).ToListAsync();
              }
          }
      }
      
      

      The following MongoDb aggregate is generated (2 lines commented with // incorrect)

      db.parents.aggregate(
          [
         {
            "$lookup":{
               "from":"children",
               "localField":"_id",
               "foreignField":"parentId",
               "as":"children"
            }
         },
         {
            "$lookup":{
               "from":"grandChildren",
               "let":{
                  "children":"$children"
               },
               "pipeline":[
                  {
                     "$match":{
                        "$expr":{
                           "$and":[
                              {
                                 "$in":[
                                    "$childId",
                                    "$$children._id"
                                 ]
                              }
                           ]
                        }
                     }
                  }
               ],
               "as":"grandChildren"
            }
         },
         {
            "$project":{
               "Id":"$_id",
               "ParentName":"$name",
               "Children":{
                  "$map":{
                     "input":"$children",
                     "as":"child",
                     "in":{
                        "Id":"$$child._id",
                        "Name":"$$child.name",
                        "GrandChildren":{
                           "$map":{
                              "input":{
                                 "$filter":{
                                    "input":"$$child.grandChildren", // This is wrong
                                    "as":"gc",
                                    "cond":{
                                       "$eq":[
                                          "$$gc.childId",
                                          "$$gc._id" // This is wrong
                                       ]
                                    }
                                 }
                              },
                              "as":"gc",
                              "in":{
                                 "Id":"$$gc._id",
                                 "Name":"$$gc.name"
                              }
                           }
                        }
                     }
                  }
               },
               "_id":0
            }
         }
      ]
      );
      

      But the proper aggregate would be (2 lines commented with // correct)

      db.parents.aggregate(
          [
         {
            "$lookup":{
               "from":"children",
               "localField":"_id",
               "foreignField":"parentId",
               "as":"children"
            }
         },
         {
            "$lookup":{
               "from":"grandChildren",
               "let":{
                  "children":"$children"
               },
               "pipeline":[
                  {
                     "$match":{
                        "$expr":{
                           "$and":[
                              {
                                 "$in":[
                                    "$childId",
                                    "$$children._id"
                                 ]
                              }
                           ]
                        }
                     }
                  }
               ],
               "as":"grandChildren"
            }
         },
         {
            "$project":{
               "Id":"$_id",
               "ParentName":"$name",
               "Children":{
                  "$map":{
                     "input":"$children",
                     "as":"child",
                     "in":{
                        "Id":"$$child._id",
                        "Name":"$$child.name",
                        "GrandChildren":{
                           "$map":{
                              "input":{
                                 "$filter":{
                                    "input":"$grandChildren", // correct
                                    "as":"gc",
                                    "cond":{
                                       "$eq":[
                                          "$$gc.childId",
                                          "$$child._id" // correct
                                       ]
                                    }
                                 }
                              },
                              "as":"gc",
                              "in":{
                                 "Id":"$$gc._id",
                                 "Name":"$$gc.name"
                              }
                           }
                        }
                     }
                  }
               },
               "_id":0
            }
         }
      ]
      );
      

      Attached is the .NET Core solution, and database archive

        1. archive.gz
          0.5 kB
        2. MongoDbTesting.zip
          356 kB

            Assignee:
            robert@mongodb.com Robert Stam
            Reporter:
            rcollette@yahoo.com Richard Collette
            Votes:
            1 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: