Search code examples
c#mongodbasp.net-core-3.1projection.net-core-3.1

How to control the projection definition in MongoDb using C#


I have a domain class like this.

public class Thing
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Dictionary<string, string> Stuff { get; set; }
}

I'm retrieving it from my DB using the following.

return await _db.Things
    .Find(x => x.Name == name)
    .SingleOrDefaultAsync(token);
    

As I noticed, there might be huge chunks of unnecessary data so I used a projection like this.

ProjectionDefinition<Thing> projection = Builders<Thing>
    .Projection
    .Include(a => a.Id)
    .Include(a => a.Name);

BsonDocument projected = await _dbContext.Things
    .Find(x => x.Name == name)
    .Project(projection)
    .SingleOrDefaultAsync(token);

This works but, naturally, cuts of all the dictionary contents. I'd like to alter the definition of projection to include the field but perform a filtration of the constituting elements. Suppose I'd only want to bring in the keys of said dictionary that start with duck. An attempt might be something like this.

ProjectionDefinition<Thing> projection = Builders<Thing>
    .Projection
    .Include(a => a.Id)
    .Include(a => a.Name)
    .Include(a => a.Stuff.Where(b => b.Key.StartsWith("duck")));

That resulted in exception as follows.

System.NotSupportedException: The expression tree is not supported: {document}{configuration}

Given my ignorance with MongoDb, I have no idea if I should add something, remove something or forget the idea all together. I also tried to work with the original type to be able to filter the stuff that way but the only solution I've got was post-fetch, basically trumming the retrieved data. I'd like to lower the payload from the DB to my service.

Thing projected = await _dbContext.Things
    .Find(x => x.Name == name)
    .Project<Thing>(projection)
    .SingleOrDefaultAsync(token);

Is it doable at all and if so, how (or at least what to google for)?

Proof of effort: code examples, general operations, tutorials, wrong answers etc. It might be out there somewhere but I failed to find it (or recognize if found).

Finally, I landed into the following - God forgive me for I know not what I'm doing. Is this at all in the right direction or is a gang of crazy donkeys going to bite me in the lower back for this?!

ProjectionDefinition<Thing, Thing> wtf = Builders<Thing>.Projection
    .Expression(a => new Thing
    {
        Id = a.Id,
        Name = a.Name,
        Stuff = a.Stuff
            .Where(b => b.Key == scope)
            .ToDictionary(b => scope, b => b.Value)
    });

Solution

  • This is a mongo shell query with MongoDB v4.2.8.

    Consider this input document:

    {
            "_id" : 1,
            "name" : "john",
            "stuff" : {
                    "mycolor" : "red",
                    "fruit" : "apple",
                    "mybook" : "programming gems",
                    "movie" : "star wars"
            }
    }
    

    The goal is to project the name and stuff fields, but stuff with only field names starting with "my".

    The aggregation query:

    db.test.aggregate([
      { 
          $project: { 
              _id: 0, 
              name: 1, 
              stuff: { 
                  $filter: { 
                      input: { $objectToArray: "$stuff" }, 
                      as: "stf", 
                      cond: { $regexMatch: { input: "$$stf.k" , regex: "^my" } }
                  }
              }
          }
      },
      { 
          $addFields: { stuff: { $arrayToObject: "$stuff" } } 
      }
    ])
    

    And, the projected output:

    {
            "name" : "john",
            "stuff" : {
                    "mycolor" : "red",
                    "mybook" : "programming gems"
            }
    }