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

MongoDB .NET Driver - Count and average on lookup field


I have two collections, one for items, and the second for ratings for that items. I want to add two values(when getting data from item collection) average of ratings and the count of ratings.

public class ShopItem 
{ //item structure
    public Guid Id { get; set; } 

    public string Name { get; set; } = string.Empty;

    public decimal Price { get; set; }

    public string Description { get; set; } = string.Empty;

    public SeasonEnum Season { get; set; }

    public DateTimeOffset CreatedDate { get; set; }
} 

public class RatingModel 
{
    public Guid Id { get; set; }

    public Guid UserId { get; set; }

    public Guid ShopItemId { get; set; }

    [Range(1,5)] 
    public Int16 Rate { get; set; }

    public DateTimeOffset CreatedDate { get; set; }
}

So I figured out that to accomplish that, first I should lookup on the ratings collection, then group to add average and count, and then use projection to remove "looked-up" field. However, after the lookup stage, the value becomes BsonDocument and I am not sure how to handle it.

FilterDefinition<ShopItem>? filter = filterBuilder.Empty;

if (filterOptions.NameToMatch != null)
{ 
    var nameFilter = filterBuilder.Where(item => item.Name.Contains(filterOptions.NameToMatch));
    filter &= nameFilter;
}

if (filterOptions.SeasonToMatch != null)
{ 
    var seasonFilter = filterBuilder.Where(item => item.Season == filterOptions.SeasonToMatch);
    filter &= seasonFilter;
}
        
var items = await itemsCollection
               .Aggregate()
               .Match(filter)
               .Lookup("Rate", "Id", "ShopItemId", "Ratings") // BsonDocument After that line
               .Group(x => x.Ratings,  // 
                   g => new { 
                       AmountOfRatings = g.Count(), 
                       AverageRating = g.Average(x => x.Ratings.Rate) // not exactly sure what syntax there
                   }
               )
               .Projection(Builders<ShopItem>.Projection.Exclude("Ratings"))
               .ToListAsync();

Solution

  • In my opinion, you don't need the $group stage. When performing $lookup, the Rate collection is joined with ShopItem collection by ShopItemId. Hence, technically the rating is grouped by ShopItemId.

    ProjectionDefinition<BsonDocument> projection = new BsonDocument
    {
        {
            "AmountOfRatings",
            new BsonDocument("$size", "$Ratings")
        },
        {
            "AverageRating",
            new BsonDocument("$avg", "$Ratings.Rate")
        }
    };
    
    var items = await itemsCollection
        .Aggregate()
        .Match(filter)
        .Lookup("Rate", "_id", "ShopItemId", "Ratings")
        .Project(projection)
        .ToListAsync();