MongoDB C# Get latest document from group

后端 未结 3 1811
星月不相逢
星月不相逢 2021-01-13 00:29

I have a group of statuses of pretend payments, each with a payment ID.

I want to get the latest status for each payment ID. The test I have creates some dummy data

3条回答
  •  温柔的废话
    2021-01-13 00:43

    There has to be a better way though.

    As of 2.5.3, you can access the current group inside of an aggregation. This lets us build a generic accessor that will retrieve the first element from a grouping via a native mongo query.

    First, a helper class for Deserialization. KeyValuePair is sealed, so we roll our own.

        /// 
        /// Mongo-ified version of 
        /// 
        class InternalKeyValuePair
        {
            [BsonId]
            public TKey Key { get; set; } 
            public T Value { get; set; }
        }
        //you may not need this method to be completely generic, 
        //but have the sortkey be the same helps
        interface IDateModified
        {
            DateTime DateAdded { get; set; }
        }
        private List GroupFromMongo(string KeyName) where T : IDateModified
        {
            //mongo linq driver doesn't support this syntax, so we make our own bsondocument. With blackjack. And Hookers. 
            BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
             {
                    _id: '$" + KeyName + @"',
                    Value: { '$first': '$$CURRENT' }
            }");
            //you could use the same bsondocument parsing trick to get a generic 
            //sorting key as well as a generic grouping key, or you could use
            //expressions and lambdas and make it...perfect.
            SortDefinition sort = Builders.Sort.Descending(document => document.DateAdded);
            List intermediateResult = getCol().Aggregate().Sort(sort).Group(groupDoc).ToList();
            InternalResult[] list = intermediateResult.Select(r => MongoDB.Bson.Serialization.BsonSerializer.Deserialize>(r)).ToArray();
            return list.Select(z => z.Value).ToList();
        }
    

    Okay..I genericized it with some help from https://stackoverflow.com/a/672212/346272

        /// 
        /// Mongo-ified version of 
        /// 
        class MongoKeyValuePair
        {
            [BsonId]
            public TKey Key { get; set; }
            public T Value { get; set; }
        }
        private MongoKeyValuePair[] GroupFromMongo(Expression> KeySelector, Expression> SortSelector)
        {
            //mongo linq driver doesn't support this syntax, so we make our own bsondocument. With blackjack. And Hookers. 
            BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
             {
                    _id: '$" + GetPropertyName(KeySelector) + @"',
                    Value: { '$first': '$$CURRENT' }
            }");
            SortDefinition sort = Builders.Sort.Descending(SortSelector);
            List groupedResult = getCol().Aggregate().Sort(sort).Group(groupDoc).ToList();
            MongoKeyValuePair[] deserializedGroupedResult = groupedResult.Select(r => MongoDB.Bson.Serialization.BsonSerializer.Deserialize>(r)).ToArray();
            return deserializedGroupedResult;
        }
    
        /* This was my original non-generic method with hardcoded strings, PhonesDocument is an abstract class with many implementations */
        public List ListPhoneDocNames() where T : PhonesDocument
        {
            return GroupFromMongo(z=>z.FileName,z=>z.DateAdded).Select(z=>z.Value).ToList();
        }
    
    
        public string GetPropertyName(Expression> propertyLambda)
        {
            Type type = typeof(TSource);
    
            MemberExpression member = propertyLambda.Body as MemberExpression;
            if (member == null)
                throw new ArgumentException(string.Format(
                    "Expression '{0}' refers to a method, not a property.",
                    propertyLambda.ToString()));
    
            PropertyInfo propInfo = member.Member as PropertyInfo;
            if (propInfo == null)
                throw new ArgumentException(string.Format(
                    "Expression '{0}' refers to a field, not a property.",
                    propertyLambda.ToString()));
    
            if (type != propInfo.ReflectedType &&
                !type.IsSubclassOf(propInfo.ReflectedType))
                throw new ArgumentException(string.Format(
                    "Expresion '{0}' refers to a property that is not from type {1}.",
                    propertyLambda.ToString(),
                    type));
    
            return propInfo.Name;
        }
    

    For bonus points, you can now easily do any of mongos other grouping operations without fighting the linq helpers. See https://docs.mongodb.com/manual/reference/operator/aggregation/group/ for all available grouping operations. Let's add a count.

        class MongoKeyValuePair
        {
            [BsonId]
            public TKey Key { get; set; }
            public T Value { get; set; }
            public long Count { get; set; }
        }
    
            BsonDocument groupDoc = MongoDB.Bson.BsonDocument.Parse(@"
             {
                    _id: '$" + GetPropertyName(KeySelector) + @"',
                    Value: { '$first': '$$CURRENT' },
                    Count: { $sum: 1 }
            }");
    

    run the aggregation the exact same as before and your count property will be filled in with the amount of documents matching your groupkey. Neat!

提交回复
热议问题