Search code examples
c#mongodbaggregation-frameworkmongodb-.net-driver

Using Facets in the Aggregation Framework C#


I would like to create an Aggregation on my data to get the total amount of counts for specific tags for a collection of books in my .Net application.

I have the following Book class.

public class Book
{
    public string Id { get; set; }

    public string Name { get; set; }

    [BsonDictionaryOptions(DictionaryRepresentation.Document)]
    public Dictionary<string, string> Tags { get; set; }
}

And when the data is saved, it is stored in the following format in MongoDB.

{ 
    "_id" : ObjectId("574325a36fdc967af03766dc"), 
    "Name" : "My First Book", 
    "Tags" : {
        "Edition" : "First", 
        "Type" : "HardBack", 
        "Published" : "2017", 
    }
}

I've been using facets directly in MongoDB and I am able to get the results that I need by using the following query:

db.{myCollection}.aggregate(
    [
        {
            $match: {
                "Name" : "SearchValue"
            }
        },
        {
            $facet: {
                 "categorizedByTags" : [ 
                     {   
                       $project :
                       { 
                         Tags: { $objectToArray: "$Tags" }
                       }
                     },
                     { $unwind : "$Tags"},
                     { $sortByCount : "$Tags"}
                  ]

            }
        },
    ]
);

However I am unable to transfer this over to the .NET C# Driver for Mongo. How can I do this using the .NET C# driver?

Edit - I will ultimately be looking to query the DB on other properties of the books as part of a faceted book listings page, such as Publisher, Author, Page count etc... hence the usage of $facet, unless there is a better way of doing this?


Solution

  • I would personally not use $facet here since you've only got one pipeline which kind of defeats the purpose of $facet in the first place...

    The following is simpler and scales better ($facet will create one single potentially massive document).

    db.collection.aggregate([
    {
        $match: {
            "Name" : "My First Book"
        }
    }, {
        $project: {
            "Tags": {
                $objectToArray: "$Tags"
            }
        }
    }, {
        $unwind: "$Tags"
    }, {
        $sortByCount: "$Tags"
    }, {
        $group: { // not really needed unless you need to have all results in one single document
            "_id": null,
            "categorizedByTags": {
                $push: "$$ROOT"
            }
        }
    }, {
        $project: { // not really needed, either: remove _id field
            "_id": 0
        }
    }])
    

    This could be written using the C# driver as follows:

    var collection = new MongoClient().GetDatabase("test").GetCollection<Book>("test");
    
    var pipeline = collection.Aggregate()
        .Match(b => b.Name == "My First Book")
        .Project("{Tags: { $objectToArray: \"$Tags\" }}")
        .Unwind("Tags")
        .SortByCount<BsonDocument>("$Tags");
    
    var output = pipeline.ToList().ToJson(new JsonWriterSettings {Indent = true});
    
    Console.WriteLine(output);
    

    Here's the version using a facet:

    var collection = new MongoClient().GetDatabase("test").GetCollection<Book>("test");
    
    var project = PipelineStageDefinitionBuilder.Project<Book, BsonDocument>("{Tags: { $objectToArray: \"$Tags\" }}");
    var unwind = PipelineStageDefinitionBuilder.Unwind<BsonDocument, BsonDocument>("Tags");
    var sortByCount = PipelineStageDefinitionBuilder.SortByCount<BsonDocument, BsonDocument>("$Tags");
    
    var pipeline = PipelineDefinition<Book, AggregateSortByCountResult<BsonDocument>>.Create(new IPipelineStageDefinition[] { project, unwind, sortByCount });
    
    // string based alternative version
    //var pipeline = PipelineDefinition<Book, BsonDocument>.Create(
    //    "{ $project :{ Tags: { $objectToArray: \"$Tags\" } } }",
    //    "{ $unwind : \"$Tags\" }",
    //    "{ $sortByCount : \"$Tags\" }");
    
    var facetPipeline = AggregateFacet.Create("categorizedByTags", pipeline);
    
    var aggregation = collection.Aggregate().Match(b => b.Name == "My First Book").Facet(facetPipeline);
    
    var output = aggregation.Single().Facets.ToJson(new JsonWriterSettings { Indent = true });
    
    Console.WriteLine(output);