views:

395

answers:

6

I need to programatically check whether a nested property/function result in a lambda expression is null or not. The problem is that the null could be in any of the nested subproperties.

Example. Function is:

 public static bool HasNull<T, Y>(this T someType, Expression<Func<T, Y>> input)
    {
      //Determine if expression has a null property
    }

Use:

person.HasNull(d=>d.addressdetails.Street)
person.HasNull(d=>d.addressdetails[1].Street)
person.HasNull(d=>d.addressdetails.FirstOrDefault().Street)
person.HasNull(d=>d.InvoiceList.FirstOrDefault().Product.Name)

In any of the examples addressdetails or street, or invoicelist or the product or the name could be null.The code will throw an exception if i try to invoke the function and some nested property is null.

Important: I don't want to use a try catch for this because that is desastrous for the debugging performance.

The reason for this approach is to quickly check for values while i don't want to forget any nulls and so cause exceptions. This is handy for reporting solutions and grids where a null on the report can just show empty and has no futher business rules.

related post: http://stackoverflow.com/questions/1420390/dont-stop-debugger-at-that-exception-when-its-thrown-and-caught

+1  A: 

You would have to take the expression apart and evaluate each bit in turn, stopping when you got a null result. This wouldn't be impossible by any means, but it would be quite a lot of work.

Are you sure this is less work than just putting explicit null guards in the code?

Jon Skeet
yes, i was hoping that the code has already been written by someone.
MichaelD
A: 

Any reason why you couldn't just do the following?

bool result;
result = addressdetails != null && addressdetails.Street != null;
result = addressdetails != null && addressdetails.Count > 1 && addressdetails[1].Street != null;
result = addressdetails != null && addressdetails.FirstOrDefault() != null && addressdetails.FirstOrDefault().Street != null;
result = addressdetails != null && addressdetails.FirstOrDefault() != null && addressdetails.FirstOrDefault().Product != null && addressdetails.FirstOrDefault().Product.Name != null;

I think these simple boolean expressions would be the best way to go, they work because of conditional evaluation... When anding (&&) terms together, the first false term will return false and stop the rest from being evaluated, so you wouldn't get any exceptions doing this.

Sam Salisbury
Because this is more error prone and slower to write. I like clean code. Or it has a null, or it doesn't. in .net there is also a getValueOrDefault to retrieve a value from a nullable int for quicker en cleaner code
MichaelD
A: 

Though it's not the answer on this exact question, the easiest way I know to achieve the same behaviour is to pass pathes to properties as enumerables of property names instead of call chains.

public static bool HasNull<T, Y>(this T someType, IEnumerable<string> propertyNames)

Then parse those enumerables in a circle or recursively using reflection. There are some disadvantages like loosing intelly sense and static name checking, but very easy to implement, which may overweigh them in some cases.

Instead of writing

person.HasNull(d=>d.addressdetails.FirstOrDefault().Street)

you'll have to write

person.HasNull(new string[] { "addressdetails", "0", "Street" })
Dmitry Tashkinov
+2  A: 

It is possible, but I'm not sure I would recommand it. Here is something that you may find usefull: it doesn't return a boolean, but instead, the leaf value of the expression if possible (no null reference).

public static class Dereferencer
{
    private static readonly MethodInfo safeDereferenceMethodInfo 
        = typeof (Dereferencer).GetMethod("SafeDereferenceHelper", BindingFlags.NonPublic| BindingFlags.Static);


    private static TMember SafeDereferenceHelper<TTarget, TMember>(TTarget target,
                                                            Func<TTarget, TMember> walker)
    {
        return target == null ? default(TMember) : walker(target);
    }

    public static TMember SafeDereference<TTarget, TMember>(this TTarget target, Expression<Func<TTarget, TMember>> expression)
    {
        var lambdaExpression = expression as LambdaExpression;
        if (lambdaExpression == null)
            return default(TMember);

        var methodCalls = new Queue<MethodCallExpression>();
        VisitExpression(expression.Body, methodCalls);
        var callChain = methodCalls.Count == 0 ? expression.Body : CombineMethodCalls(methodCalls);
        var exp = Expression.Lambda(typeof (Func<TTarget, TMember>), callChain, lambdaExpression.Parameters);
        var safeEvaluator = (Func<TTarget, TMember>) exp.Compile();

        return safeEvaluator(target);
    }

    private static Expression CombineMethodCalls(Queue<MethodCallExpression> methodCallExpressions)
    {
        var callChain = methodCallExpressions.Dequeue();
        if (methodCallExpressions.Count == 0)
            return callChain;

        return Expression.Call(callChain.Method, 
                               CombineMethodCalls(methodCallExpressions), 
                               callChain.Arguments[1]);
    }

    private static MethodCallExpression GenerateSafeDereferenceCall(Type targetType,
                                                                    Type memberType,
                                                                    Expression target,
                                                                    Func<ParameterExpression, Expression> bodyBuilder)
    {
        var methodInfo = safeDereferenceMethodInfo.MakeGenericMethod(targetType, memberType);
        var lambdaType = typeof (Func<,>).MakeGenericType(targetType, memberType);
        var lambdaParameterName = targetType.Name.ToLower();
        var lambdaParameter = Expression.Parameter(targetType, lambdaParameterName);
        var lambda = Expression.Lambda(lambdaType, bodyBuilder(lambdaParameter), lambdaParameter);
        return Expression.Call(methodInfo, target, lambda);
    }

    private static void VisitExpression(Expression expression, 
                                        Queue<MethodCallExpression> methodCallsQueue)
    {
        switch (expression.NodeType)
        {
            case ExpressionType.MemberAccess:
                VisitMemberExpression((MemberExpression) expression, methodCallsQueue);
                break;
            case ExpressionType.Call:
                VisitMethodCallExpression((MethodCallExpression) expression, methodCallsQueue);
                break;
        }
    }

    private static void VisitMemberExpression(MemberExpression expression, 
                                              Queue<MethodCallExpression> methodCallsQueue)
    {
        var call = GenerateSafeDereferenceCall(expression.Expression.Type,
                                               expression.Type,
                                               expression.Expression,
                                               p => Expression.PropertyOrField(p, expression.Member.Name));

        methodCallsQueue.Enqueue(call);
        VisitExpression(expression.Expression, methodCallsQueue);
    }

    private static void VisitMethodCallExpression(MethodCallExpression expression, 
                                                  Queue<MethodCallExpression> methodCallsQueue)
    {
        var call = GenerateSafeDereferenceCall(expression.Object.Type,
                                               expression.Type,
                                               expression.Object,
                                               p => Expression.Call(p, expression.Method, expression.Arguments));

        methodCallsQueue.Enqueue(call);
        VisitExpression(expression.Object, methodCallsQueue);
    }
}

You can use it this way:

var street = person.SafeDereference(d=>d.addressdetails.Street);
street = person.SafeDereference(d=>d.addressdetails[1].Street);
street = person.SafeDereference(d=>d.addressdetails.FirstOrDefault().Street);
var name = person.SafeDereference(d=>d.InvoiceList.FirstOrDefault().Product.Name);

Warning : this is not fully tested, it should work with methods and properties, but probably not with extension methods inside the expression.

Edit : Ok, it can't handle extension methods for now (e.g. FirstOrDefault) but it's still possible to adjust the solution.

Romain Verdier
Wow, if it really works, that must be great, but personnaly I wouldn't like to debug this method ;)
Dmitry Tashkinov
just tested it, it works, but as you say not with extension methods. but it's a good start
MichaelD
+1  A: 

We definately need a null-safe dereferencing operator in C#, but until then look at this question, which provides a slightly different but also neat solution to the same problem.

jeroenh
A: 

I blogged about, it works in VB.NET (I translated to C#, but I didn't test the C# version, check it out).

Checkout: How would you check car.Finish.Style.Year.Model.Vendor.Contacts.FirstOrDefault().FullName for null? :-)

Dim x = person.TryGetValue(
    Function(p) p.addressdetail(1).FirstOrDefault().Product.Name)
Shimmy