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
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!