[{"data":1,"prerenderedAt":210},["ShallowReactive",2],{"/2025/12/02-strongly-typed-left-outer-join-on-mongodb":3},{"id":4,"title":5,"body":6,"date":201,"description":57,"extension":202,"meta":203,"navigation":204,"path":205,"robots":206,"seo":207,"stem":208,"__hash__":209},"posts/2025/12/02 strongly-typed-left-outer-join-on-mongodb.md","02 Strongly Typed Left Outer Join On Mongodb",{"type":7,"value":8,"toc":198},"minimark",[9,18,21,37,39,42,44,58,60,70,78,80,96,104,106,109,117,119,122,130,132,135,143,145,158,166,169,177,179,190],[10,11,13],"post-title",{":date":12},"date",[14,15,17],"h1",{"id":16},"strongly-typed-left-outer-join-on-mongodb","Strongly Typed Left Outer Join on MongoDB",[19,20],"br",{},[22,23,24,25,36],"p",{},"Only on the road that you'll see potholes, not on the map. So, I want to perform left outer join on two collections in MongoDB. Lookup has been great (",[26,27,30],"span",{"className":28},[29],"text-blue-600",[31,32,33],"a",{"href":33,"rel":34},"https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/",[35],"nofollow","), but I want it strongly typed in C#.",[19,38],{},[22,40,41],{},"Let's take the Movies and Reviews example where:",[19,43],{},[45,46,47],"code-block",{},[48,49,54],"pre",{"className":50,"code":52,"language":53},[51],"language-text","public class Movie\n{\n    public ObjectId Id { get; set; }\n    public string Title { get; set; }\n    public IEnumerable\u003CReview> Reviews { get; set; } = [];\n}\n\npublic class Review\n{\n    public ObjectId Id { get; set; }\n   \n    [BsonElement(\"movie_id\")]\n    public ObjectId MovieId { get; set; }\n\n    public int Rating { get; set; }\n}\n","text",[55,56,52],"code",{"__ignoreMap":57},"",[19,59],{},[22,61,62,63],{},"To simply join all movies to reviews, we can do using ",[55,64,69],{"className":65},[66,67,68],"bg-gray-200","p-2","rounded","Aggregate().Lookup()",[45,71,72],{},[48,73,76],{"className":74,"code":75,"language":53},[51],"IMongoCollection\u003CMovie> movieCollection = ...;\nIMongoCollection\u003CReview> reviewCollection = ...;\n\nvar moviesWithReviews = await movieCollection.Aggregate()\n  .Lookup\u003CMovie, Review, Movie>(\n    foreignCollection: reviewCollection,\n    localField: movie => movie.Id,\n    foreignField: review => review.MovieId,\n    @as: movie => movie.Reviews).ToListAsync();\n",[55,77,75],{"__ignoreMap":57},[19,79],{},[22,81,82,83,87,88,95],{},"But if we need additional condition, for example ",[55,84,86],{"className":85},[66,67,68],"Movie.Title == \"Beauty and the Beast\"",", we need to use pipeline. ",[26,89,91],{"className":90},[29],[31,92,93],{"href":93,"rel":94},"https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#std-label-lookup-multiple-joins",[35],". So, the query to MongoDB is similar to:",[45,97,98],{},[48,99,102],{"className":100,"code":101,"language":53},[51],"db.movie.aggregate( [\n   {\n      $lookup:\n         {\n           from : \"reviews\",\n           localField : \"_id\",\n           foreignField : \"movie_id\",\n           let : { pipeline_title: \"$title\" },\n           pipeline : [\n              { $match :\n                 { $expr :\n                      { $eq: [ \"$$pipeline_title\", \"Beauty and the Beast\" ] }\n                 }\n              }\n           ],\n           as : \"Reviews\"\n         }\n    }\n] )\n",[55,103,101],{"__ignoreMap":57},[19,105],{},[22,107,108],{},"Notes the use of let. It is because the pipeline has no access to the input. And to access the pipeline variable, we need double dollar sign ($$). From the documentation:",[45,110,111],{},[48,112,115],{"className":113,"code":114,"language":53},[51],"The pipeline cannot access fields from input documents. Instead, define variables for the document fields using the let option and then reference the variables in the pipeline stages.\n",[55,116,114],{"__ignoreMap":57},[19,118],{},[22,120,121],{},"To convert to C#, there's no Lookup overload that's similar to the query above. But we can do the following query and utilize one of the overloads:",[45,123,124],{},[48,125,128],{"className":126,"code":127,"language":53},[51],"db.movie.aggregate( [\n   {\n      $lookup:\n         {\n           from : \"reviews\",\n           let : { pipeline_title: \"$title\", pipeline_id: \"$_id\" },\n           pipeline : [\n              { $match :\n                 { $expr :\n                      { $and: [\n                        { $eq: [ \"$movie_id\", \"$$pipeline_id\" ]},\n                        { $eq: [ \"$$pipeline_title\", \"Beauty and the Beast\" ] }\n                      ]}\n                      \n                 }\n              }\n           ],\n           as : \"Reviews\"\n         }\n    }\n] )\n",[55,129,127],{"__ignoreMap":57},[19,131],{},[22,133,134],{},"In C#, it becomes:",[45,136,137],{},[48,138,141],{"className":139,"code":140,"language":53},[51],"var lookupPipeline = new EmptyPipelineDefinition\u003CReview>()\n  .Match(new BsonDocument(\"$expr\",\n          new BsonDocument(\"$and\", new BsonArray\n          {\n            new BsonDocument(\"$eq\", new BsonArray { \"$movie_id\", \"$$pipeline_id\" }),\n            new BsonDocument(\"$eq\", new BsonArray { \"$$pipeline_title\", \"Beauty and the Beast\" \n          })\n  })));\n\nvar moviesWithReviews = await movieCollection.Aggregate()\n  .Lookup\u003CMovie, Review, IEnumerable\u003CReview>, Movie>(\n      foreignCollection: reviewCollection,\n      let: new BsonDocument { { \"pipeline_title\": \"$title\" }, { \"pipeline_id\", \"$_id\" } },\n      lookupPipeline: lookupPipeline,\n      @as: new ExpressionFieldDefinition\u003CMovie, IEnumerable\u003CReview>>(movie => movie.Reviews))\n  .ToListAsync();\n",[55,142,140],{"__ignoreMap":57},[19,144],{},[22,146,147,148,152,153,157],{},"If we want the lookupPipeline to be strongly typed, especially on the ",[55,149,151],{"className":150},[66,67,68],"Review.MovieId"," field we are can't do the following since pipeline variable is only accessible with ",[55,154,156],{"className":155},[66,67,68],"$expr"," operator. As far as I experienced, I don't see any C# equivalent for the operator.",[45,159,160],{},[48,161,164],{"className":162,"code":163,"language":53},[51],"var lookupPipeline = new EmptyPipelineDefinition\u003CReview>()\n  .Match(review => review.MovieId == \"$$pipeline_id\" && \"$$pipeline_title\" == \"Beauty and the Beast\");\n",[55,165,163],{"__ignoreMap":57},[22,167,168],{},"And the following doesn't work either:",[45,170,171],{},[48,172,175],{"className":173,"code":174,"language":53},[51],"var filterBuilder = Builders\u003CReview>.Filter;\n\nvar filter = filterBuilder.And(\n    filterBuilder.Eq(review => review.MovieId, \"$$pipeline_id\"),\n    filterBuilder.Eq(\"$$pipeline_title\", \"Beauty and the Beast\"));\n",[55,176,174],{"__ignoreMap":57},[19,178],{},[22,180,181,182,189],{},"After more trials and errors, I found MongoDB LINQ Syntax for Aggregation. ",[26,183,185],{"className":184},[29],[31,186,187],{"href":187,"rel":188},"https://www.mongodb.com/docs/drivers/csharp/current/aggregation/linq/#lookup--",[35],". The one that finally works look like:",[45,191,192],{},[48,193,196],{"className":194,"code":195,"language":53},[51],"var movieCollectionQuery = movieCollection.AsQueryable();\n\nvar lookupResults = await movieCollectionQuery.Lookup\u003CMovie, Review, Review>(\n            reviewCollection,\n            (movie, reviews) => reviews.Where(review => review.MovieId == movie.Id && movie.Title == \"Beauty and the Beast\")).ToListAsync();\n\nvar moviesWithReviews = [..lookupResults.Select(result =>\n{\n    var movie = result.Local;\n    movie.Reviews = result.Results;\n    return movie;\n})];\n",[55,197,195],{"__ignoreMap":57},{"title":57,"searchDepth":199,"depth":199,"links":200},2,[],"2025-12-02T00:00:00.000Z","md",{},true,"/2025/12/02-strongly-typed-left-outer-join-on-mongodb",null,{"title":5,"description":57},"2025/12/02 strongly-typed-left-outer-join-on-mongodb","gwjZlR1WTHuTzaYJkJ-jG-N-HpfQiVzUDp0D_WfJpxg",1785167452341]