Home > database >  How to return a List from nested lists with LINQ?
How to return a List from nested lists with LINQ?

Time:12-21

I have to return all rooms which doesn't contain Cat or Owl. If a room contains even one forbidden animal, it cannot be added to the list which we'd like to return. It's going to be a method for a service which is used as an API endpoint.

public async Task<List<Room>> GetRoomForRabbitOwners() 
{
}
public enum PetType : byte
{
        Cat,
        None,
        Rabbit,
        Owl
}

Here is the data structure :

[
    {
        "id": 1,
        "capacity": 5,
        "residents": [
            {
                "id": 2,
                "name": "Tom",
                "houseType": 2,
                "petType": 1,
                "room": null
            },
            {
                "id": 4,
                "name": "Nicol",
                "houseType": 2,
                "petType": 3,
                "room": null
            }
        ]
    },
    {
        "id": 2,
        "capacity": 5,
        "residents": [
            {
                "id": 3,
                "name": "Rambo",
                "houseType": 2,
                "petType": 2,
                "room": null
            }
        ]
    },
    {
        "id": 3,
        "capacity": 1010,
        "residents": []
    },
    {
        "id": 4,
        "capacity": 10,
        "residents": []
    },
    {
        "id": 5,
        "capacity": 15,
        "residents": []
    }
]

A bunch of times, I managed to write an expression which seemed good but in the end it failed by returning a List of "Tenant" (class) , since only "Room" (class) allowed.

CodePudding user response:

It seems like your post needs a little more information. I.e. what do you mean with tenant vs resident? Also your data structure defines a room as containing residents. Do you want the room returned w/out it's residents?

Here's a solution that returns a list of rooms w/out Cats & Owls:

void Main()
{
    var data = File.ReadAllText(Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.Desktop), 
        "data.json"));
    
    var allRooms = JsonConvert.DeserializeObject<List<Room>>(data);

    var filteredRooms = GetRoomForRabbitOwners(allRooms).Dump();
}

public List<Room> GetRoomForRabbitOwners(List<Room> rooms)
{
    return rooms
        .Where(room => !room.Residents.Any(resident =>
            (PetType)resident.PetType == PetType.Cat ||
            (PetType)resident.PetType == PetType.Owl))      
        .ToList();
}

public enum PetType : byte
{
    Cat = 1,
    None = 2,
    Rabbit = 3,
    Owl = 4
}

public class Resident
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int HouseType { get; set; }
    public byte PetType { get; set; }
    public int? Room { get; set; }
}

public class Room
{
    public int Id { get; set; }
    public int Capacity { get; set; }
    public List<Resident> Residents { get; set; }
}

CodePudding user response:

So you have Rooms and Residents. There is a one to many relation between Rooms and Residents: Every Room has zero or more Residents, every Resident is the Pet in exactly one Room.

It is wise too separate your problem into two subproblems:

  • Read your source. Get a sequence of "Rooms with their zero or more Residents"
  • Give a sequence of Rooms with their Residents, give me all Rooms that have neither a Cat nor an Owl as Resident.

.

class Resident
{
    public int Id {get; set;}
    public string Name {get; set;}
    public HouseType HouseType {get; set;}
    public PetType PetType {get; set;}
    ... // etc
}

class Room
{
    public int Id {get; set;}
    public int Capacity {get; set;}
    public virtual ICollection<Resident> Residents {get; set;}
}

I choose the Residents as a virtual ICollection<Resident> for several reasons. First of all, it is not a List, because Resident[2] doesn't have a defined meaning: you are not certain whether Cats come before Dogs. ICollection<...> contains all methods you need to handle the Residents: you can Add / Remover / Count Residents. The method is virtual, so procedures that will actual read your source to create the Rooms are free to decide what specific class they want to use to store the Residents (List<Resident> or Resident[...], or something else.

The following procedure will read your source into a sequence of "Rooms with their Residents":

public IEnumerable<Room> GetRoomsWithResidents()
{
    ...
}

You are probably more familiar with the data format that your sequence has, so you can implement it. The advantage of a separate procedure is, that it will be easy to unit test. Easy to change: if later you decide to use a database to store the Rooms, or a CSV-file. Changes will be minimal.

Requirement Give a sequence of Rooms, and a sequence of forbidden PetTypes, give me all Rooms that have no Residents of these PetTypes at all.

I'll write this as an extension method, so you can intertwine this with other LINQ methods. If you are not familiar with extension methods, consider to read Extension Methods demystified.

public IEnumerable<Room> ToRoomsWithoutForbiddenPets(this IEnumerable<Room> rooms,
    IEnumerable<PetType> forbiddenPets)
{
    // TODO: check input not null. Raise exceptions
    return rooms.Where(room => !room.Residents
        .Select(resident => resident.PetType)
        .Intersect(forbiddenPets)
        .Any());
}

In words: from every Room in the sequence of Rooms, keep only those Rooms that meet the following requirement: take the PetType from every Resident in the Room. This is a sequence of PetTypes. Intersect this sequence with the forbiddenPetTypes. Only keep the Room if this intersection does not have any elements (is empty).

Usage:

IEnumerable<PetType> forbiddenPets = new PetType[] {PetType.Cat, PetType.Owl}

IEnumerable<Room> validRooms = GetRoomsWithResidents()
    .ToRoomsWithoutForbiddenPets(forbiddentPets);

The method ToRoomsWithoutForbiddenPets is also easy to understand, easy to unit test and easy to maintain.

  • Related