1

I'm trying to query some data and projecting to a class with fewer properties by sending an expression (C# mongo driver version 2.7.3). I am trying to understand why a specific expression fails. The failure greatly limits the user from writing a common projection and forces him to write the projection inline in every call. This is a simplified example:

private IMongoCollection<MyOriginalClass> _collection;

class MyOriginalClass // imagine this class has many more properties
{
  public int ID { get; set; }
}

class MyProjectedClass
{
  public int ID { get; set; }
}

void DoWork()
{
  var data1 = GetData(lib => new MyProjectedClass { ID = lib.ID }); // succeeds
  var data2 = GetData(lib => ToProjected(lib)); // Fails in mongo driver: Index was out of range. Must be non-negative and less than the size of the collection.Parameter name: index
}

IEnumerable<MyProjectedClass> GetData(Expression<Func<MyOriginalClass, MyProjectedClass>> projection)
{       
  return _collection
      .Aggregate()
      .Project(Builders<MyOriginalClass>.Projection.Expression(projection))
      .ToList();
}

MyProjectedClass ToProjected(MyOriginalClass orig)
{
    return new MyProjectedClass {ID = orig.ID};
}
Aman Agnihotri
  • 2,973
  • 1
  • 18
  • 22
Metheny
  • 1,112
  • 1
  • 11
  • 23

1 Answers1

2

First (succeeded) usage is an expression which mongo driver can look into at the runtime to become to know that ID = lib.ID. Specifically here is NewExpression.

E.g. Visual Studio allow for expression visualization under debugger, and for first one it shows:

.Lambda #Lambda1<System.Func`2[ConsoleApp1.Program+MyOriginalClass,ConsoleApp1.Program+MyProjectedClass]>(ConsoleApp1.Program+MyOriginalClass $lib)
{
    .New ConsoleApp1.Program+MyProjectedClass(){
        ID = $lib.ID
    }
}

Second (failed) usage is an expression with just a call to ToProjected, ToProjected is being compiled into IL, and at the runtime mongo driver cannot retrieve the knowledge that ID = lib.ID (at least not in such an easy way as with expressions). Specifically here is MethodCallExpression. And the visualization of the second expression is:

.Lambda #Lambda1<System.Func`2[ConsoleApp1.Program+MyOriginalClass,ConsoleApp1.Program+MyProjectedClass]>(ConsoleApp1.Program+MyOriginalClass $lib)
{
    .Call ConsoleApp1.Program.ToProjected($lib)
}

ToProject could be re-written as:

Expression<Func<MyOriginalClass, MyProjectedClass>> ToProjected()
{
    return lib => new MyProjectedClass { ID = lib.ID };
}

And used as:

var data2 = GetData(ToProjected());
Renat
  • 7,718
  • 2
  • 20
  • 34
  • So there is no difference between a compiled expression (Func) and a MethodCallExpression? MethodCallExpression cannot be traversed? – Metheny Apr 15 '19 at 14:54
  • 1
    Well it's kind of no difference, MethodCallExpression just calls a compiled code, as expression tree it's very shallow, and provides little info at the runtime. In contrast NewExpression could be a bigger expression tree with more info. And yes, MethodCallExpression cannot be traversed (without decompiling) – Renat Apr 15 '19 at 15:06