0

I have one list with few records, list has another object inside it. Based on inner object I am using OrderByDescending. Inner object is null in few places so it is throwing NullReferenceException.

items.OrderByDescending(i=> i.Obj_Announcement.CreatedDate).ToList()

When Obj_Announcement is null the query throws an exception.

Not duplicate: I have object inside OrderByDescending not single field.

How can I solve this issue?

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
R15
  • 13,982
  • 14
  • 97
  • 173
  • 1
    Possible duplicate of [LINQ order by null column where order is ascending and nulls should be last](https://stackoverflow.com/questions/6461479/linq-order-by-null-column-where-order-is-ascending-and-nulls-should-be-last) – pasha goroshko Feb 18 '19 at 09:32
  • 3
    To solve this you should at first decide wether how the `null` values should be sorted. Should they be lowest or highest? – René Vogt Feb 18 '19 at 09:33
  • @RenéVogt - I need latest records on top, this way I need. – R15 Feb 18 '19 at 09:40
  • The Net Library doesn't allow a null for a DateTime object. Usually I use a new DateTime which sets default Date to 1/1/1. Then I test for null by checking if the date is greater than 1/1/1. – jdweng Feb 18 '19 at 10:02
  • You should order hasValue of inner object then order list by it's value: `items.OrderByDescending(i=> i.Obj_Announcement.CreatedDate.HasValue).ThenByDescending(i=> i.Obj_Announcement.CreatedDate.Value)` – Muhammad Vakili Feb 18 '19 at 10:07

3 Answers3

3

The neat method would be to separate your LINQ query from the way you define which item comes before another item. In other word: what is the sort order of Item X and Item Y

For this we need to create a class that implements IComparer<Item>

class Item
{
     public Announcement Announcement {get; set;} // your inner object, might be null
     ...
}

class Announcement
{
     public DateTime CreatedDate {get; set;}
     ...
}

Suppose you you have two Items, one of them has a null Anouncement.

Item item1 = new Item {Announcement = null};
Item item2 = new Item {Announcement = new AnnounceMent{CreatedDate=new DateTime(2000, 1, 1)}};

It's up to you to decide which comes first. Let's assume you want the null items at the end.

class ItemComparer : Comparer<Item>
{
     public static readonly ByCreatedDate = new ItemComparer();

     private static readonly IComparer<DateTime> dateComparer = Comparer<DateTime>.default;

     public override int CompareTo(Item x, Item y)
     {
         // TODO: decide what to do if x or y null: exception? comes first? comes last

         if (x.Announcement == null)
         {
             if (y.Announcement == null)
                 // x and y both have null announcement
                 return 0;
             else
                 // x.Announcement null, y.Announcement not null: x comes last:
                 return +1;
         }
         else
         {
             if (y.Announcement == null)
                 // x.Announcement not null and y.Announcement null; x comes first
                 return -1;
             else
                 // x.Announcement not null, y.Announcement not null: compare dates
                 return dateComparer.CompareTo(x.CreatedDate, y.CreateDate)
         }
    }
}

Usage:

(improved after comment by Peter Wurzinger)

var result = items.OrderByDescending(item => item, ItemComparer.ByCreatedDate);

Note: If you decide to change the sort order of your Items, all you have to do is change the ItemComparer class. All your LINQ statements that order your Items will then use the new sort order.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • +1, this is great answer! But I think there is no overload for `OrderByDescending(IEnumerable, IComparer)` according to https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderbydescending. Imho the call should therefore look like `items.OrderByDescending(item => item, ItemComparer.ByCreatedDate);` And it should be noted, that it is most likely not translatable when used with `IQueryable` like OR-mapping, OData or whatever. – Peter Wurzinger Feb 18 '19 at 11:27
2

You basically got two options depending on the resulting set you're expecting.

  1. Exclude the instances, which's Obj_Announcement - property is null. But imho that's not the scope of the question and would change the beavior of the program.

  2. Explicitly decide on how to deal with nulls.

When looking at the documentation for OrderByDescending (https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderbydescending) one comes across the keySelector parameter. Since it's a Func<TSource, TKey> it should be able to deal edge cases a TSource instance could show - for example nulls on a child-object, in your case. So imho the easiest solution on how to deal with that would be deciding whether you want to prepend or append the nulls to the list by

items.OrderByDescending(i=> i.Obj_Announcement?.CreatedDate ?? DateTime.MinValue).ToList()

or

items.OrderByDescending(i=> i.Obj_Announcement?.CreatedDate ?? DateTime.MaxValue).ToList()

But be aware, that DateTime.MinValue and DateTime.MaxValue are only assumptions as default values. They do not reflect the actual state of the object.

Peter Wurzinger
  • 391
  • 2
  • 9
1
items.OrderByDescending(i=> i.Obj_Announcement?.CreatedDate ?? DateTime.MinValue).ToList()

should work, as if the value is null, it will return DateTime.MinValue (meaning null values will be at the back), and otherwise, you will just retrieve the CreatedDate.

Please note that I am not in front of a pc right now though, so could not test.

iSpain17
  • 2,502
  • 3
  • 17
  • 26