MongoDB .NET Driver:为商品集合添加评分数量与平均值统计
解决方案
你的核心问题是误用了Group阶段,并且没有正确处理Lookup后的强类型映射。Lookup后每个商品文档已经关联了对应的评分数组,不需要对整个集合分组,而是要针对每个商品的评分数组单独计算统计值。下面是修正后的实现方案:
1. 定义结果DTO
首先创建一个DTO来承载商品信息+评分统计,避免修改原ShopItem类:
public class ShopItemWithRatingStats { 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 int AmountOfRatings { get; set; } public double? AverageRating { get; set; } // 用double适配MongoDB的$avg返回值,无评分时返回null }
2. 修正聚合管道
方式一:强类型聚合(推荐)
通过泛型Lookup保持强类型,直接在投影阶段计算统计值:
// 假设你有评分集合的引用 var ratingsCollection = database.GetCollection<RatingModel>("Rate"); 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) // 强类型关联评分集合,将结果映射到临时的带Ratings属性的类 .Lookup<ShopItem, RatingModel, ShopItemWithRatings>( ratingsCollection, localField: item => item.Id, foreignField: rating => rating.ShopItemId, @as: itemWithRatings => itemWithRatings.Ratings ) // 投影构造最终结果,计算评分统计 .Projection(itemWithRatings => new ShopItemWithRatingStats { Id = itemWithRatings.Id, Name = itemWithRatings.Name, Price = itemWithRatings.Price, Description = itemWithRatings.Description, Season = itemWithRatings.Season, CreatedDate = itemWithRatings.CreatedDate, AmountOfRatings = itemWithRatings.Ratings.Count, // 无评分时返回null,避免空数组计算平均值出错 AverageRating = itemWithRatings.Ratings.Any() ? (double?)itemWithRatings.Ratings.Average(r => r.Rate) : null }) .ToListAsync();
需要临时定义一个继承自ShopItem的中间类来承载关联的评分:
public class ShopItemWithRatings : ShopItem { public List<RatingModel> Ratings { get; set; } = new List<RatingModel>(); }
方式二:BsonDocument聚合
如果不想新增中间类,可以直接用BsonDocument构造聚合阶段,最后映射到DTO:
var items = await itemsCollection .Aggregate() .Match(filter) // 关联评分集合,生成BsonDocument .Lookup("Rate", "Id", "ShopItemId", "Ratings") // 添加评分统计字段 .AddFields(new BsonDocument { { "AmountOfRatings", new BsonDocument("$size", "$Ratings") }, { "AverageRating", new BsonDocument("$avg", "$Ratings.Rate") } }) // 移除临时的Ratings字段 .Project(Builders<BsonDocument>.Projection.Exclude("Ratings")) // 映射到目标DTO .As<ShopItemWithRatingStats>() .ToListAsync();
关键说明
- 不需要
Group阶段:Group是用来对多个文档做分组统计(比如按季节分组统计所有商品的评分),而你需要的是单个商品的评分统计,Lookup后每个文档已经对应一个商品+它的所有评分,直接对数组计算即可。 - 空评分处理:当商品没有评分时,
$avg会返回null,强类型方式中需要用Any()判断避免空数组调用Average()报错。 - 类型映射:通过泛型Lookup或者
As<T>方法可以将BsonDocument转换回强类型对象,解决Lookup后类型变为BsonDocument的问题。
内容的提问来源于stack exchange,提问作者algorithms
相关产品推荐
相关产品推荐

