[CSHARP-3227] Builders<T>.Projection fails to deserialize arrays nested in subdocuments Created: 20/Oct/20  Updated: 10/Nov/22

Status: Backlog
Project: C# Driver
Component/s: Builders
Affects Version/s: 2.11.0
Fix Version/s: None

Type: Bug Priority: Major - P3
Reporter: John Knoop Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: size-small
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

I've verified this behaviour both on Windows as well as in a Linux container


Issue Links:
Related
Epic Link: Quick Wins
Case:

 Description   

I believe I’ve found a bug related to projection, specifically when you map an array that is a deep descendant of the document root.

Suppose this is my model:

public class DummyContainer
{
    public DummyContainer(IList<Dummy> dummies)
    {
 
        Dummies = dummies;
        this.Id = ObjectId.GenerateNewId().ToString();
    }
 
    public string Id { get; private set; }
    public IList<Dummy> Dummies { get; private set; }
}
 
public class Dummy
{
    public Dummy(string name)
    {
        this.Name = name;
        this.Id = ObjectId.GenerateNewId().ToString();
    }
 
    public string Id { get; private set; }
    public string Name { get; private set; }
}

First, an example that works:

Let’s say I insert one document of type DummyContainer into an empty collection:

collection.InsertOneAsync(new DummyContainer(new[]
{
    new Dummy("SomeValue1"),
    new Dummy("SomeValue2"),
}));

And then I load all documents from that collection, projected into this type:

class DummyContainerWithDummyNames
{
    public string ContainerId { get; set; }
    public IEnumerable<string> DummyNames { get; set; }
}

Here we go:

var dummyNamesByUsingFindAsync = await (await collection.FindAsync(Builders<DummyContainer>.Filter.Empty, new FindOptions<DummyContainer, DummyContainerWithDummyNames> { 
        Projection = Builders<DummyContainer>.Projection.Expression(x => new DummyContainerWithDummyNames
        {
            ContainerId = x.Id,
            DummyNames = x.Dummies.Select(d => d.Name)
        })
    })).ToListAsync();
 
var dummyNamesUsingQuery = await collection.AsQueryable()
        .Select(x => new DummyContainerWithDummyNames
        {
            ContainerId = x.Id,
            DummyNames = x.Dummies.Select(d => d.Name)
        })
        .ToListAsync();

Result:

Both dummyNamesByUsingFindAsync and dummyNamesUsingQuery contain one document, and in both cases the property DummyNames is populated with ["SomeValue1", "SomeValue2"].

An example that shows the bug:

Ok, in order to reproduce the bug, we’re gonna wrap the DummyContainer in a DummyContainerWrapper and let this be our root entity:

 

public class DummyContainerWrapper
{
    public DummyContainerWrapper(DummyContainer container)
    {
        Container = container;
        this.Id = ObjectId.GenerateNewId().ToString();
    }
 
    public string Id { get; private set; }
    public DummyContainer Container { get; private set; }
}

We’re gonna insert such a document to a new collection:

 

await collection.InsertOneAsync(new DummyContainerWrapper(new DummyContainer(new[]
{
    new Dummy("SomeValue1"),
    new Dummy("SomeValue2"),
})));

 

And just like before, we’re gonna query the collection for all documents and project them into the same type we used above:

 

var dummyNamesByUsingFindAsync = await (await collection.FindAsync(Builders<DummyContainerWrapper>.Filter.Empty, new FindOptions<DummyContainerWrapper, DummyContainerWithDummyNames> { 
    Projection = Builders<DummyContainerWrapper>.Projection.Expression(x => new DummyContainerWithDummyNames
    {
        ContainerId = x.Id,
        DummyNames = x.Container.Dummies.Select(d => d.Name)
    })
})).ToListAsync();
 
var dummyNamesUsingQuery = await collection.AsQueryable()
    .Select(x => new DummyContainerWithDummyNames
    {
        ContainerId = x.Id,
        DummyNames = x.Container.Dummies.Select(d => d.Name)
    })
    .ToListAsync();

And this is where we see the bug:

dummyNamesUsingQuery has one element, who’s DummyNames }}property is an {{IEnumerable<string> (the concrete type is List<string>) populated with{{ ["SomeValue1", "SomeValue2"]}}.

dummyNamesByUsingFindAsync }}on the other hand also has one element, but it´s {{DummyNames }}property is of type {{System.Linq.Enumerable.SelectListIterator<MyTestProgram.Dummy, string>, and the values are both null.

 

 

 



 Comments   
Comment by James Kovacs [ 03/Nov/20 ]

Hi, John. Thank you for reporting this bug in our Find/Project implementation. We have repro'd the issue and will investigate further.

Repro:

using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
 
namespace CSHARP_3227
{
  class Program
  {
    static void Main()
    {
      var client = new MongoClient();
      var db = client.GetDatabase("CSHARP3227");
      var collection = db.GetCollection<OuterContainer>("test");
      collection.DeleteMany(FilterDefinition<OuterContainer>.Empty);
 
      var document = new OuterContainer(
        new[] { new Dummy("outerDummy1"), new Dummy("outerDummy2")},
        new InnerContainer(new[] {
          new Dummy("innerDummy1"),
          new Dummy("innerDummy2")
      }));
      collection.InsertOne(document);
 
      var filter = Builders<OuterContainer>.Filter.Empty;
      var projection = Builders<OuterContainer>.Projection.Expression(x => new DummyContainerWithDummyNames
        {
          ContainerId = x.Id,
          InnerDummyNames = x.Inner.Dummies.Select(d => d.Name),
          OuterDummyNames = x.Dummies.Select(d => d.Name)
        });
      var dummyNamesByUsingFindProject = collection.Find(filter).Project(projection).ToList();
      Console.WriteLine("From Find().Project():");
      dummyNamesByUsingFindProject.ForEach(Console.WriteLine);
 
      var dummyNamesUsingAsQueryable = collection.AsQueryable()
        .Select(x => new DummyContainerWithDummyNames
            {
              ContainerId = x.Id,
              InnerDummyNames = x.Inner.Dummies.Select(d => d.Name),
              OuterDummyNames = x.Dummies.Select(d => d.Name)
            })
        .ToList();
      Console.WriteLine("From AsQueryable<>.Select():");
      dummyNamesUsingAsQueryable.ForEach(Console.WriteLine);
    }
  }
 
  public class OuterContainer
  {
    public OuterContainer(IList<Dummy> dummies, InnerContainer container)
    {
      Id = ObjectId.GenerateNewId().ToString();
      Inner = container;
      Dummies = dummies;
    }
 
    public string Id { get; private set; }
    public InnerContainer Inner { get; private set; }
    public IList<Dummy> Dummies { get; private set; }
  }
  
  public class InnerContainer
  {
    public InnerContainer(IList<Dummy> dummies)
    {
      Dummies = dummies;
    }
 
    public IList<Dummy> Dummies { get; private set; }
  }
 
  public class Dummy
  {
    public Dummy(string name)
    {
      Name = name;
    }
 
    public string Name { get; private set; }
  }
 
  public class DummyContainerWithDummyNames
  {
    public string ContainerId { get; set; }
    public IEnumerable<string> InnerDummyNames { get; set; }
    public IEnumerable<string> OuterDummyNames { get; set; }
 
    public override string ToString() {
      return $"{ContainerId}: InnerDummies [{string.Join(", ", InnerDummyNames)}], OuterDummies: [{string.Join(", ", OuterDummyNames)}]";
    }
  }
}

Output:

From Find().Project():
5fa1d02a9e368f31d5fed8ae: InnerDummies [, ], OuterDummies: [outerDummy1, outerDummy2]
From AsQueryable<>.Select():
5fa1d02a9e368f31d5fed8ae: InnerDummies [innerDummy1, innerDummy2], OuterDummies: [outerDummy1, outerDummy2]

Generated at Wed Feb 07 21:44:44 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.