Jul 24 2013

Null safe accessor

Category: C# | Visual Studio — Duke @ 06:54

It is happened to me many time to have to accesso a long hierarchi of object just to display a value to the user, and possibly this hierarchy contains null values

So the case is a.B.C.D.E.F where a is my object reference B, C, D, E are child of a in a long hierarchy of property accessor, and finally F is a simple integer o a string that i don't care about but the user want to see (if present)

 

It turns out that some languages have the so called  Groovy's safe dereferencing operator  (see http://stackoverflow.com/questions/10815641/elegant-way-to-avoid-nullreferenceexception-in-c-sharp)

but C# have not :(

 

Stack overwlow is full of smart solutions, some very elegant (see some here http://stackoverflow.com/questions/4244225/c-sharp-if-null-then-null-expression) but allof these are in someway suboptimal

 

the one i like more is

 

bossName =Employee.NullSafe( e => e.Supervisor).NullSafe( s => s.Boss).NullSafe( b => b.Name);

 

but is too verbose :(

 

so i've ended up with an alternate solution (that cames in two flavours to tell the true)

 

flavour one (plain vanilla)

ObjectExtension.NullSafe(() => a.B.C);

 

flavour two (tropical explosion)

a.NullSafe(x => x.B.C);

 

both of these can optionally specify which value to return in case the path doesn't complete, otherwise they will return the default for the type you are trying to access.

 

there are mainly two implementation of this (i've tryed both)

one is to rewrite the expression so to add all the step-in-path-not-null checks, then compile the expression and execute.

for example the expression

x=> x.B.C

 

will be transformed in

if(x!=null && x.B!=null && x.B.C != null) return x.B.C;

else return default(T);

this works and it is very cool, but it have some performance penatiels due to the fact that the compiler is ingaged each time an access is tryed.

caching expression is not Always obvious and have serious difficulties. on the other side not caching them will pollute your appdomain with tons of anonymus function call :(

 

the other way is to use the good-old reflection by examining the expression and compiling sub expression only in very peculiar case that should (almost) never happen

 

this is my code

public static TValue NullSafe<T, TValue>(this T root, Expression<Func<T, TValue>> path, TValue defaultValue = default(TValue))
        {
            var pathAsLambda = path as LambdaExpression;
            if (pathAsLambda.Parameters.Count > 1) throw  new Exception("too much parameters in the lambda expression");
            var rootParam = pathAsLambda.Parameters.FirstOrDefault();
 
 
            var value = NullSafePath(path, root, rootParam);
            if (value == null) return defaultValue;
            return (TValue)value;
        }
 
        public static TValue NullSafe<TValue>(Expression<Func<TValue>> path, TValue defaultValue = default(TValue))
        {
            var value = NullSafePath(path, null, null);
            if (value == null) return defaultValue;
            return (TValue)value;
        }
 
        private static object NullSafePath(Expression path, object root, ParameterExpression rootParam)
        {
            if (path == null) return root;
 
            var pathAsMember = path as MemberExpression;
            if (pathAsMember != null)
            {
                object newRoot = NullSafePath(pathAsMember.Expression, root, rootParam);
                if (newRoot == null) return null;
 
                var propertyMember = pathAsMember.Member as PropertyInfo;
                if (propertyMember != null)
                {
                    return propertyMember.GetValue(newRoot, null);
                }
                var fieldMember = pathAsMember.Member as FieldInfo;
                if (fieldMember != null)
                {
                    return fieldMember.GetValue(NullSafePath(pathAsMember.Expression, root, rootParam));
                }
            }
 
            var pathAsMethod = path as MethodCallExpression;
            if (pathAsMethod != null)
            {
                object newRoot = NullSafePath(pathAsMethod.Object, root, rootParam);
                if (newRoot == null) return null;
 
                var parameters = EvaluateParameters(pathAsMethod.Arguments, root, rootParam);
 
                return pathAsMethod.Method.Invoke(newRoot, parameters);
            }
 
            var pathAsConstant = path as ConstantExpression;
            if (pathAsConstant != null)
            {
                return pathAsConstant.Value;
            }
 
            var pathAsParameter = path as ParameterExpression;
            if (pathAsParameter != null)
            {
                if (pathAsParameter.Type == root.GetType()) return root;
                else throw new ArgumentException("Type of root object doesn't match the type of the first element in the expression");
            }
 
            var pathAsLambda = path as LambdaExpression;
            if (pathAsLambda != null)
            {
                if (pathAsLambda.Parameters.Count == 0)
                {
                    return NullSafePath(pathAsLambda.Body, null, null);
                }
 
                if (pathAsLambda.Parameters.Count > 1) throw new InvalidOperationException("Maximum one parameter is allowed in the path expression");
                if (root == null) return null;
                if (pathAsLambda.Parameters.First().Type != root.GetType()) new ArgumentException("Type of root object doesn't match the type of the first element in the expression");
                return NullSafePath(pathAsLambda.Body, root, rootParam);
            }
 
            throw new ArgumentException(string.Format("Unsupported expression type", path.ToString()));
        }
 
        private static object[] EvaluateParameters(ReadOnlyCollection<Expression> readOnlyCollection, object root, ParameterExpression rootParam)
        {
            if (readOnlyCollection.IsNullOrEmpty()) return null;
            return readOnlyCollection.Select(exp =>
                {
                    LambdaExpression lambda = Expression.Lambda(exp, rootParam);
                    var compiledExpression = lambda.Compile();
                    return compiledExpression.DynamicInvoke(root);
                }).ToArray();
        }

 

 

 

Tags:

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading