1

I try to count unread messages for a user.

On my model, I have a property, LastMessageDate that contains the date on the last created message in the group chat. I have also a Members property (list) that contains the members in the group chat. Each member has the UserId and LastReadDate properties. The LastReadDate is updated when the user writes a new message in the group chat or when the user loads messages from the group chat.

Now I want to count the number of chats where a specific user has unread messages (The messages are stored in another collection). I try this:

var db = GetGroupCollection();

var filter = Builders<ChatGroup>.Filter.Where(p => p.Members.Any(m => m.UserId == userId && m.LastReadDate < p.LastMessageDate));
return await db.CountDocumentsAsync(filter);

But I receive the following error:

The LINQ expression: {document}{Members}.Where((({document}{UserId} == 730ddbc7-5d03-4060-b9ef-2913d0b1d7db) AndAlso ({document}{LastReadDate} < {document}{LastMessageDate}))) has the member "p" which can not be used to build a correct MongoDB query.

What should I do? Is there a better solution?

Yong Shun
  • 35,286
  • 4
  • 24
  • 46
mrcode
  • 465
  • 2
  • 5
  • 16
  • As I know, currently it's not supported to use parent linq income argument (`p`) in nested linq queries, you can try a raw query in form like: `await db.CountDocumentsAsync("your query in a raw json/bson form ")` – dododo Jul 13 '22 at 15:29

2 Answers2

4

Based on the provided data in the comment, I think the aggregation query is required to achieve the outcome.

  1. $set - Set Members field

    1.1. $filter - With Members array as input, filter the document(s) with matching the current document's UserId and LastMessageDate is greater than ($gt) the current document's LastReadDate.

  2. $match - Filter the document with Members is not an empty array.

db.groups.aggregate([
  {
    "$set": {
      Members: {
        $filter: {
          input: "$Members",
          cond: {
            $and: [
              {
                $eq: [
                  "$$this.UserId",
                  1
                ]
              },
              {
                $gt: [
                  "$LastMessageDate",
                  "$$this.LastReadDate"
                ]
              }
            ]
          }
        }
      }
    }
  },
  {
    $match: {
      Members: {
        $ne: []
      }
    }
  }
])

Sample Mongo Playground


For C# syntax, either you can directly provide the query as a string or convert the query to BsonDocument syntax.

Note that the query above will return the array of documents, hence you will need to use System.Linq to count the returned document(s).

using System.Linq;

var pipeline = new BsonDocument[]
{
    new BsonDocument("$set", 
        new BsonDocument("Members", 
            new BsonDocument("$filter", 
                new BsonDocument
                { 
                    { "input", "$Members" },
                    { "cond", new BsonDocument
                        (
                            "$and", new BsonArray
                            {
                                new BsonDocument("$eq", 
                                    new BsonArray { "$$this.UserId", userId }),
                                new BsonDocument("$gt",
                                    new BsonArray { "$LastMessageDate", "$$this.LastReadDate" })
                            }
                        )
                    }
                }
            )
        )
    ),
    new BsonDocument("$match",
        new BsonDocument("Members",
            new BsonDocument("$ne", new BsonArray())))

};

var db = GetGroupCollection();

return (await db.AggregateAsync<BsonDocument>(pipeline))
    .ToList()
    .Count;
Yong Shun
  • 35,286
  • 4
  • 24
  • 46
  • thanks, works perfect. One thing, this query will return all matching documents to the c# code, then the c# code will count the items/list? If so, is it posible to return only the number of the items? – mrcode Jul 14 '22 at 19:36
  • *this query will return all matching documents to the c# code, then the c# code will count the items/list?* Yes, the C# side does the counting. *If so, is it posible to return only the number of the items?* Did you mean MongoDB side to return count only? If yes, I afraid that it is not possible for MongoDB so far. – Yong Shun Jul 15 '22 at 00:17
  • 1
    Performance-wise, since you are performing count, you return the document with `_id` field only and omit the rest field(s) via `$project` from MongoDB side. – Yong Shun Jul 15 '22 at 01:43
0

When you want to query a nested list of a document, ElemMatch is your solution, Try

var filter = builder.ElemMatch(o => o.Members,m => m.UserId == userId && m.LastReadDate < p.LastMessageDate);
Nadav Hury
  • 564
  • 4
  • 12
  • I can't access o.LastMessageDate, Error CS0103 The name 'o' does not exist in the current context – mrcode Jul 13 '22 at 17:46
  • First parameter of the ElemMatch function accepts the members array, that's the o. Second parameter is the lambda > m => m.UserId == userId && m.LastReadDate < p.LastMessageDate – Nadav Hury Jul 13 '22 at 18:16
  • Where do you get p in p.LastMessageDate from? – mrcode Jul 13 '22 at 18:24
  • After taking another look, I think the problem is that you query a nested list based on a value on the document that holds it. when you ask for m.LastReadDate < p.LastMessageDate the m is the nested list item, and the p is the document that holds it. it is not possible with the solution I gave you. – Nadav Hury Jul 13 '22 at 18:29
  • Try using aggregation, you want to have the LastMessageDate field projected into the ElemMatch query, and then you will be able to use it. Take a look here https://stackoverflow.com/questions/45412506/compare-embedded-document-to-parent-field-with-mongodb – Nadav Hury Jul 13 '22 at 18:56
  • Thanx, i tried this: https://mongoplayground.net/p/YkNGD86ucUO but i can't get any result :/ UserId 1 and 2 should return 1 each and userid 3 nothing. – mrcode Jul 13 '22 at 20:20