LINQ - EF Core - Return object that contains two objects (referenced by property) in nested list

Viewed 398

I know there are a million LINQ questions so I'm sorry for asking. I've looked around, and I can't quite find the right answer. I suspect this is very easy but I'm new to LINQ and EF core.

I have the following List of type Room that is returned from a DB query (I only include one for brevity):

[
    {
        "roomId": 2,
        "roomName": "test",
        "users": [
            {
                "userId": 2,
                "username": "seconduser",
                "password": "demo",
                "token": null,
                "refreshToken": null,
                "alias": "test",
                "friends": null
            },
            {
                "userId": 3,
                "username": "thirduser",
                "password": "demo",
                "token": null,
                "refreshToken": null,
                "alias": "test",
                "friends": null
            }
        ]
    }
]

Great. I want a LINQ query (preferably in fluent syntax, but I appreciate any help) that will query this list with many more Room's and return only one (I know there will only be one) that satisfies the following criterion:

Only two users in the room, and both of the users are identified by userId's passed into the method.

So if the method receives the following object:

{
    "friendId": 1,
    "requestUserId": 3
}

The room quoted above will be returned, and no other.

I hope this is clear.

Thanks in advance!

Edit - fair enough, I include now a couple of the attempts that got me closer

I tried populating two users so that I could ask if room.Users.Contains() like so:

var requestingUser = UserService.GetSingleUser(request.RequestUserId);
            
var friendUser = UserService.GetSingleUser(srequest.FriendId);

var test = allRooms.FirstOrDefault(room => room.Users.Count == 2 && room.Users.Contains(friendUser) && room.Users.Contains(requestingUser);

However the comparison by User doesn't go through. Is this just a problem of how I'm populating the User? Anyway, I know I shouldn't have to do that.

Then I tried what seems like the rigth approach in terms of direction, of just comparing the ID's like so:

var test = allRooms.First(room => room.Users.Any(user =>
    user.userId == request.FriendId && user.userId == request.RequestUserId));

But this returns null, yet I have confirmed that there is a room that satisfies the conditions I specify.

What gives me trouble is that EF core has generated a database schema that is unfamiliar to me. In just SQL I would have tables ROOM, USER, ROOMUSER and inner join the ROOMUSER where both USER.id exist... and distinct, or something like that anyway.

Further edit:

I include the models I am using to construct the DB below. Initially I did not because the JSON I included is the result of querying for all rooms and including their users (an ICollection within Room).

    public class User
    {
        public int UserId { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string Token { get; set; }
        public string RefreshToken { get; set; }
        public string Alias { get; set; }
        
        public ICollection<User> Friends { get; set; }
    }

    public class Room
    {
        public int RoomId { get; set; }
        public string RoomName { get; set; }
        
        public ICollection<User> Users { get; set; }
    }

So as I described how the tables would be if I had crafted it by hand, the idea is that User and Room are distinct entities (primary entities), and there would be a joining table to suggest their relationship. Conceptually, a User is in a Room.

What might make things more clear as well is the initial JSON is returned by the following LINQ:

var allRooms = db.Rooms.Include(x => x.Users).ToList();

Thus it should not be too difficult to ask for a room with the above conditions.

Admittedly, as I said, I have not worked with EF core ever, and barely ever with LINQ. I might be making a silly error...

1 Answers

This one is close

room => room.Users.Any(user =>
    user.userId == request.FriendId && user.userId == request.RequestUserId)

but needs some adjustments

  • And (&&) operator is not appropriate - the id couldn't be equal to the both passed ids at the same time. It should be either one or another, i.e. what you need is Or (||) operator instead

    user.userId == request.FriendId || user.userId == request.RequestUserId

  • Any is not appropriate either. It would return a room having just one of the passed users, or both, but with additional users in there. What you need is All instead.

    room.Users.All(user => user.userId == request.FriendId || user.userId == request.RequestUserId)

However All also returns true for empty sets (i.e. when room.Users is empty), so it must be combined with another call to eliminate that case

room.Users.Any() && room.Users.All(user =>
    user.userId == request.FriendId || user.userId == request.RequestUserId)

which would produce the desired result, but is inefficient because is involving two subqueries instead of one.

Hence the best would be to use what I call "conditional counting" and compare to resulting count to two (2):

room.Users.Count(user =>
    user.userId == request.FriendId || user.userId == request.RequestUserId) == 2

which could also be expressed more generically (as it would support more than 2) with input like this

// could contain more than two ids and doesn't need to be array
var userIds = new [] { request.FriendId, request.RequestUserId }.AsEnumerable();

and the query condition like

room.Users.Count(user => userIds.Contains(user.userId)) == userIds.Count()
Related