14

I'm using this dynamic linq orderby function which I got from here.

This works fine with nested properties so I could do this:

var result = data.OrderBy("SomeProperty.NestedProperty");

The problem is that if SomeProperty is null then performing the OrderBy on the NestedProperty throws the infamous "Object reference not set to an instance of an object".

My guess is that I need to customize the following lines to handle the exception:

expr = Expression.Property(expr, pi);

// Or

LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);    

I thought about creating a statement body where I could in the worst case scenario use a try catch but that didn't work as you can't have statement bodies within orderby linq statements: "A lambda expression with a statement body cannot be converted to an expression tree"

I'm lost over here, any suggestions on how I can accomplish this?

By the way, this is for Linq to Objects, not database related.

Community
  • 1
  • 1
  • I guess this line `expr = Expression.Property(expr, pi);` sets `expr` to null and further code does not handle it. The easiest way to fix it is `expr = Expression.Property(expr, pi) ?? default(T);`. However you'll need to check if you ok with applied order in this case. – Tommi Jul 15 '13 at 06:13
  • It's a good point, actually that would make the sorting work wrong, ideally the nulls should be "grouped" together. –  Jul 15 '13 at 06:14
  • see if this helps you in any way http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet?lq=1 – Ehsan Jul 15 '13 at 06:15
  • I suppose they will: if property is string, default will be String.Empty and all items with null property will be pushed to back or front of collection depending on asc./desc. sort. – Tommi Jul 15 '13 at 06:16
  • @Tommi I've tried your default(T) to see how it worked but I can't compile it, I have a "Operator ?? cannot be applied..." error –  Jul 15 '13 at 06:18
  • Indeed. Since `expr` is whole expression, we can't do it. So now easier way is yours `OrderByLambda()`; Func generic args is `MyType, string`, I think it's enough to return `String.Empty` in `catch` – Tommi Jul 15 '13 at 06:27
  • It could but I cant find out how to translate that into the expression syntax as my example was IF this wasn't dynamic. –  Jul 15 '13 at 06:30
  • Sorry, was AFK. Actually, your goal is somehow possible with `Expression.TryCatch`, but I'm really unfamiliar with expressions techinque. However, I've got alternative solution, based on simple reflection traversing. Check answer with example. – Tommi Jul 15 '13 at 09:19

2 Answers2

10
static void Main(string[] args)
{
    var data = new List<MyType>() {
        new MyType() { SomeProperty = new Inner() { NestedProperty = "2" }},
        new MyType() { SomeProperty = new Inner() { NestedProperty = "1" }},
        new MyType() { SomeProperty = new Inner() { NestedProperty = "3" }},
        new MyType(),
    }.AsQueryable();
    var sorted = data.OrderBy(x => GetPropertyValue(x, "SomeProperty.NestedProperty"));

    foreach (var myType in sorted)
    {
       try
       {
          Console.WriteLine(myType.SomeProperty.NestedProperty);
       }
       catch (Exception e)
       {
          Console.WriteLine("Null");
       }
    }
}

public static object GetPropertyValue(object obj, string propertyName)
{
    try
    {
        foreach (var prop in propertyName.Split('.').Select(s => obj.GetType().GetProperty(s)))
        {
            obj = prop.GetValue(obj, null);
        }
        return obj;
    }
    catch (NullReferenceException)
    {
        return null;
    }
}
Tommi
  • 3,199
  • 1
  • 24
  • 38
  • Outstanding, this one looks simpler than Marc Gravel's answer, which makes me wonder. Nevertheless, all my testing seems to work just fine and you made me a happy men. –  Jul 15 '13 at 09:35
  • Very clever. Nice trick with the recursion. Microsoft should provide this as an extension method in future frameworks. – arviman Aug 19 '14 at 06:32
  • What if instead of SomeProperty you have a List? How would the above code need to be modified to deal with that case? – Kappacake Feb 09 '18 at 11:47
  • 1
    @demonicdaron I don't know how you want to access the property, but assuming you want something like `GetPropertyValue(x, "SomeProperty[0].NestedProperty")`, you will need to modify `GetPropertyValue` to additionally parse the index in square brackets. Then you can just use this index on a current property casted to `IList`, I guess. – Tommi Feb 12 '18 at 11:14
6

How about generics:

Helper Method:

public static Expression<Func<TEntity, TResult>> GetExpression<TEntity, TResult>(string prop)
        {
            var param = Expression.Parameter(typeof(TEntity), "p");
            var parts = prop.Split('.');

            Expression parent = parts.Aggregate<string, Expression>(param, Expression.Property);
            Expression conversion = Expression.Convert(parent, typeof (object));

            var tryExpression = Expression.TryCatch(Expression.Block(typeof(object), conversion),
                                                    Expression.Catch(typeof(object), Expression.Constant(null))); 

            return Expression.Lambda<Func<TEntity, TResult>>(tryExpression, param);
        }

Sample Hierarchy:

public class A
    {
        public A(B b)
        {
            B = b;
        }

        public B B { get; set; }
    }

    public class B
    {
        public B(C c)
        {
            C = c;
        }

        public C C { get; set; }
    }

    public class C
    {
        public C(int id)
        {
            this.Id = id;
        }

        public int Id { get; set; }
    }

Example:

var list = new List<B>
            { 
                new B(new A(new C(1))),
                new B(new A(new C(2))),
                new B(new A(new C(3))),
                new B(new A(null)),
                new B(null)
            }.AsQueryable();

var ordered = list.OrderByDescending(GetExpression<B, Object>("AProp.CProp.Id"));

Output:

3
2
1
Null
Null
Daniel Conde Marin
  • 7,588
  • 4
  • 35
  • 44
  • 1. The solution will not work if TResult is a value type, because it would try to cast null to the value type, and result in another NullReferenceException. Would be better to use Expression.Default(typeof(TResult)) instead of Expression.Constant(null) 2. There's no need to convert to an object if using the suggestion above 3. I would recommend narrowing down the exceptions caught by using Expression.Catch(typeof(NullReferenceException) ... – jonh Aug 16 '22 at 19:42