Home > Mobile >  Searching for an item in a list that's nested inside another list based on a property value in
Searching for an item in a list that's nested inside another list based on a property value in

Time:11-18

Is there a way to search for an item in a list that's nested inside another list based on a property value using LINQ?

Given the follow models below, for a given Order (variable customerOrder), I want to return the earliest order date (Date) where the Day is "Sunday".

models:

public class Order
{
    public int Id { get; set; }
    public List<OrderLine> OrderLines { get; set; }
}

public class OrderLine
{
    public string Description { get; set; }
    public List<OrderDate> OrderDates { get; set; }
}

public class OrderDate
{
    public DateTime Date { get; set; }
    public string Day { get; set; }
}

code:

var dates = new List<DateTime>();
foreach(var a in customerOrder.OrderLines)
{
    var orderDate = a.OrderDates.Where(x => x.DateTypeId.Equals("Sunday")).FirstOrDefault();
    dates.Add(orderDate.ActualDate);
}
dates.OrderBy(d => d.Date);

return dates.FirstOrDefault();

CodePudding user response:

EDIT More elegant query

You can use Linq to achieve your result.

Here is a query that would closely mimick your code.

customerOrder.OrderLines
    .Select(ol => ol.OrderDates
        .Where(x => x.Day.Equals("Sunday"))
        .FirstOrDefault())
    .Where(d => d != null)
    .OrderBy(d => d.Date)
    .FirstOrDefault();

which could be more elegantly rewritten as:

customerOrder.OrderLines
    .SelectMany(ol => ol.OrderDates)
    .OrderBy(d => d.Date)
    .FirstOrDefault(d => d.Day == "Sunday");

Here is a Linqpad query with some test data and dump for you to try. Simply copy and paste in Linqpad.

void Main()
{
    var customerOrder = new Order
    {
        Id = 1,
        OrderLines = Enumerable
            .Range(0, 10)
            .Select(i => new OrderLine
            {
                Description = $"Line Description {i}",
                OrderDates = Enumerable.Range(0, 10)
                    .Select(j => new OrderDate
                    {
                        Date = DateTime.Now.AddDays(i j),
                        Day = DateTime.Now.AddDays(i j).DayOfWeek.ToString()
                    })
                    .ToList()
            })
            .ToList()
        }
        .Dump();

    customerOrder.OrderLines
        .SelectMany(ol => ol.OrderDates)
        .OrderBy(d => d.Date)
        .FirstOrDefault(d => d.Day == "Sunday")
        .Dump();
}

// You can define other methods, fields, classes and namespaces here

public class Order
{
    public int Id { get; set; }
    public List<OrderLine> OrderLines { get; set; }
}

public class OrderLine
{
    public string Description { get; set; }
    public List<OrderDate> OrderDates { get; set; }
}

public class OrderDate
{
    public DateTime Date { get; set; }
    public string Day { get; set; }
}

On a side note the OrderDate class is not necessary. The DateTime type has a property DayOfWeek that you can use to test is a Date is a Sunday.

DayOfWeek is an enum so you can simply test MyDate.DayOfWeek == DayOfWeek.Sunday rather than relying on a string for that purpose.

CodePudding user response:

First of all your code will not work as intended. dates.OrderBy(d => d.Date); doesn't work: OrderBy returns an IEnumerable, it doesn't change the original collection. You should either use List.Sort` or do this:

dates = dates.OrderBy(d => d.Date).ToList()

Secondly, you use FirstOrDefault: it has an overload that accepts predicate to search with; so the Where call is not needed. In addition FirstOrDefault will return null if nothing found. If this is a possible scenario, you should consider checking whether orderDate is null:

var dates = new List<DateTime>();
foreach(var a in customerOrder.OrderLines)
{
    var orderDate = a.OrderDates.FirstOrDefault(x => x.DateTypeId.Equals("Sunday"));
    if (orderDate is {})
    {
        dates.Add(orderDate.ActualDate);
    }
}
dates = dates.OrderBy(d => d.Date).ToList();

return dates.FirstOrDefault();

That should work fine. But it hard to guess what aspects of behavior of your code samples are intended and what are not. You ask about searching, but say nothing about OrderBy part. Could you clarify this part, please?

Answering the question, if by better you mean more compact way, you can go with something like this:

var result = customerOrder.OrderLines
    .SelectMany(a => a.OrderDates)
    .OrderBy(d => d.Date)
    .FirstOrDefault(x => x.DateTypeId.Equals("Sunday"));
return result;

You shouldn't be bothered with better way now; firstly you should start with at least working way. I suggest you to learn how to do these things both using Linq and without using Linq.

CodePudding user response:

Better is a bit subjective, but you can use the Enumerable.SelectMany extension method to flatten the OrderDate instances into one sequence.

Then you can use the Enumerable.Where extension method to filter the dates that are "Sunday".

Then you can use the Enumerable.Min extension method to get the minimum date.

All of this can be chained together into a single statement.

DateTime earliestSunday = customeOrder
   .OrderLines
   .SelectMany(ol => ol.OrderDates)
   .Where(od => od.Day == "Sunday")
   .Min(od => od.Date);
  • Related