ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Done
    • Priority: Unknown
    • 4.0.0, 3.next
    • Affects Version/s: 3.9.0, 3.8.1
    • Component/s: LINQ, LINQ3
    • None
    • None
    • Dotnet Drivers
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      Queryable.Concat() throws a NullReferenceException during expression translation (reproducible by just calling .ToString() on the composed queryable, before any server round-trip) whenever the second argument is a queryable with zero LINQ stages applied - e.g. otherCollection.AsQueryable() passed directly, with no .Where()/.Select()/etc. applied. This is arguably the most common way to call Concat between two collections. Queryable.Union() is not affected and correctly produces a single pipeline (with the extra $group/$replaceRoot dedup stages Union requires).

      MongoDB.Driver 3.8.1 and 3.9.0, .NET 10. No MongoDB server connection is required to reproduce - the crash happens entirely client-side during LINQ expression translation.

      How to Reproduce

      Minimal repro (no live MongoDB server connection is required - the crash happens during client-side expression translation):

       

       

      using MongoDB.Driver;
      
      var client = new MongoClient("mongodb://localhost:27017");
      
      var db = client.GetDatabase("test");
      
      IQueryable<BsonDocument> a = db.GetCollection<BsonDocument>("a").AsQueryable();
      
      IQueryable<BsonDocument> b = db.GetCollection<BsonDocument>("b").AsQueryable();
      
      var union = a.Concat(b);
      
      Console.WriteLine(union.ToString()); // throws NullReferenceException
      
      

       

       

      Expected result: a single aggregation pipeline with a { $unionWith: "b" } stage (the short form, since the second collection has no additional pipeline stages) - analogous to what Queryable.Union() already produces correctly today.

       

      Actual result:

       

       
      
      System.NullReferenceException: Object reference not set to an instance of an object.
      
         at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.ConcatMethodToPipelineTranslator.Translate(TranslationContext context, MethodCallExpression expression)
      
         at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators.ExpressionToPipelineTranslator.Translate(TranslationContext context, Expression expression)
      
         at MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators.ExpressionToExecutableQueryTranslator.Translate[TDocument,TOutput](MongoQueryProvider`1 provider, Expression expression, ExpressionTranslationOptions translationOptions)
      
         at MongoDB.Driver.Linq.Linq3Implementation.MongoQuery`2.ToString()
      
       
      
      

       

      Additional Background (root cause analysis):

       

      In ConcatMethodToPipelineTranslator.cs:

       

       

      var secondPipeline = ExpressionToPipelineTranslator.Translate(secondContext, secondQueryable.Expression);
      
      if (secondPipeline.Ast.Stages.Count == 0)
      
      {
      
          secondPipeline = null;
      
      }
      
      pipeline = pipeline.AddStage(
      
          AstStage.UnionWith(secondCollectionName, secondPipeline.Ast), // NRE: secondPipeline is null here
      
          pipeline.OutputSerializer);
      
       
      
      AstUnionWithStage is explicitly designed to accept a null pipeline (see AstUnionWithStage.RenderWith(), which branches on _pipeline == null to render the short { $unionWith: "collName" } form instead of { $unionWith: { coll, pipeline } }). So nulling out secondPipeline when it has zero stages is intentional - the bug is that the call site then dereferences it unconditionally instead of using secondPipeline?.Ast.
      
      

       

       

      This was introduced by commit a3d3d4f5933acb52b84a37b391f7edcff87fd8a ("CSHARP-5448: Refactor AstFilterField and AstPipeline to remove reference to serializers", 2024-12-19). Before that commit, Translate returned the raw AstPipeline and the (possibly-null) secondPipeline was passed directly to UnionWith(...) - safe. The refactor changed the return type to a TranslatedPipeline wrapper and mechanically updated both secondPipeline.Stages.Count -> secondPipeline.Ast.Stages.Count (correct) and AstStage.UnionWith(secondCollectionName, secondPipeline) -> AstStage.UnionWith(secondCollectionName, secondPipeline.Ast) (should have been secondPipeline?.Ast).

       

      Suggested fix:

       

      • AstStage.UnionWith(secondCollectionName, secondPipeline.Ast),
      • + AstStage.UnionWith(secondCollectionName, secondPipeline?.Ast),

      Workaround: Queryable.Union() is not affected and produces a correct (if slightly heavier, due to the $group/$replaceRoot dedup stages) single pipeline.

            Assignee:
            Adelin Mbida Owona
            Reporter:
            Matheus Ernan Reichert (EXT)
            None
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: