Skip to content

Instantly share code, notes, and snippets.

@vermie
Last active December 18, 2015 02:59
Show Gist options
  • Save vermie/5715434 to your computer and use it in GitHub Desktop.
Save vermie/5715434 to your computer and use it in GitHub Desktop.

Rewrites something like obj.Property.InvokeMethod().Field to a null-safe version:

var v1 = obj;
if (v1 == null)
    return defaultValue;
var v2 = v1.Property;
if (v2 == null)
    return defaultValue;
var v3 = v2.InvokeMethod();
if (v3 == null)
    return defaultValue;
var v4 = v3.Field;

return v4;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace Expressions
{
public static class Coalesce
{
public static TResult Get<T, TResult>(T parameter, Expression<Func<T, TResult>> expression, TResult coalesceValue)
{
var lambda = CreateDelegate(expression, coalesceValue);
var result = lambda(parameter);
return result;
}
public static Func<T, TResult> CreateDelegate<T, TResult>(Expression<Func<T, TResult>> expression, TResult coalesceValue)
{
var coalesceExpression = CoalescenceVisitor.Visit<T, TResult>(expression, coalesceValue);
var lambda = coalesceExpression.Compile();
return lambda;
}
}
public class CoalescenceVisitor : ExpressionVisitor
{
private readonly LabelTarget _returnLabel;
private readonly ConstantExpression _coalesceValue;
private readonly List<ParameterExpression> _variables;
public CoalescenceVisitor(LabelTarget returnLabel, ConstantExpression coalesceValue)
{
_returnLabel = returnLabel;
_coalesceValue = coalesceValue;
_variables = new List<ParameterExpression>();
}
public static Expression<Func<T, TResult>> Visit<T, TResult>(Expression<Func<T, TResult>> expression, TResult coalesceValue)
{
var coalesceConstant = Expression.Constant(coalesceValue, typeof(TResult));
var returnLabel = Expression.Label(typeof(TResult));
var parameter = expression.Parameters.Single();
var visitor = new CoalescenceVisitor(returnLabel, coalesceConstant);
var visitedExpression = visitor.Visit(expression.Body);
return Expression.Lambda<Func<T, TResult>>(
Expression.Block(
visitor._variables,
Expression.Goto(returnLabel, visitedExpression),
Expression.Label(returnLabel, coalesceConstant)),
parameter);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
var isStatic = node.Method.IsStatic;
var isExtension = isStatic && node.Method.GetCustomAttributes(false).OfType<ExtensionAttribute>().Any();
if (isStatic && !isExtension)
throw new ArgumentException("unsupported method call: " + node, "node");
Expression methodCall, objectExpression;
ParameterExpression variable;
if (isExtension)
objectExpression = base.Visit(node.Arguments[0]);
else
objectExpression = base.Visit(node.Object);
variable = Expression.Variable(objectExpression.Type);
_variables.Add(variable);
Expression store = Expression.Assign(variable, objectExpression);
Expression test = Expression.Equal(variable, Expression.Constant(null));
if (isExtension)
methodCall = Expression.Call(node.Method, new[] { variable }.Concat(node.Arguments.Skip(1)));
else
methodCall = Expression.Call(variable, node.Method, node.Arguments);
var expression = Expression.Block(
store,
Expression.IfThen(test, Expression.Return(_returnLabel, _coalesceValue)),
methodCall);
return expression;
}
protected override Expression VisitMember(MemberExpression node)
{
var visitedOwner = base.Visit(node.Expression);
ParameterExpression variable = Expression.Variable(visitedOwner.Type);
_variables.Add(variable);
var store = Expression.Assign(variable, visitedOwner);
var member = Expression.MakeMemberAccess(variable, node.Member);
var test = Expression.Equal(variable, Expression.Constant(null));
var expression = Expression.Block(
store,
Expression.IfThen(test, Expression.Return(_returnLabel, _coalesceValue)),
member);
return expression;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Expressions.Test.Unit
{
[TestClass]
public class CoalescingExpressionVisitorTests
{
private class Class
{
public string Property { get; set; }
public IEnumerable<string> Collection { get; set; }
}
private class First
{
public Second Second { get; set; }
}
private class Second
{
public Third Third { get; set; }
}
private class Third
{
public string Value { get; set; }
}
[TestMethod]
public void ChainedPropertyAccess()
{
var coalesceValue = "foo";
var lambda = Coalesce.CreateDelegate<First, string>(f => f.Second.Third.Value, coalesceValue);
var returnValue = lambda(null);
Assert.AreEqual(coalesceValue, returnValue);
var first = new First();
returnValue = lambda(first);
Assert.AreEqual(coalesceValue, returnValue);
first.Second = new Second();
returnValue = lambda(first);
Assert.AreEqual(coalesceValue, returnValue);
first.Second.Third = new Third();
returnValue = lambda(first);
Assert.AreEqual(first.Second.Third.Value, returnValue);
first.Second.Third.Value = "bar";
returnValue = lambda(first);
Assert.AreEqual(first.Second.Third.Value, returnValue);
}
[TestMethod]
public void NonNullRootValue_ReturnsPropertyValue()
{
var coalesceValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Property, coalesceValue);
var root = new Class() { Property = "notFoo" };
var returnValue = lambda(root);
Assert.AreEqual(root.Property, returnValue);
}
[TestMethod]
public void NullRootValue_ReturnsCoalesceValue()
{
var coalesceValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Property, coalesceValue);
var returnValue = lambda(null);
Assert.AreEqual(coalesceValue, returnValue);
}
[TestMethod]
public void CollectionIsNull_ToString_ReturnsCoalesceValue()
{
var coalesceValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.ToString(), coalesceValue);
var root = new Class();
var returnValue = lambda(root);
Assert.AreEqual(coalesceValue, returnValue);
}
[TestMethod]
public void CollectionIsNull_FirstOrDefault_ReturnsCoalesceValue()
{
var coalesceValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.FirstOrDefault(), coalesceValue);
var root = new Class();
var returnValue = lambda(root);
Assert.AreEqual(coalesceValue, returnValue);
}
[TestMethod]
public void CollectionIsEmpty_FirstOrDefault_ReturnsNull()
{
var defaultValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.FirstOrDefault(), defaultValue);
var root = new Class() { Collection = new string[] { } };
var returnValue = lambda(root);
Assert.AreEqual(null, returnValue);
}
[TestMethod]
public void CollectionIsNotEmpty_FirstOrDefault_ReturnsFirst()
{
var defaultValue = "foo";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.FirstOrDefault(), defaultValue);
var root = new Class() { Collection = new string[] { "bar" } };
var returnValue = lambda(root);
Assert.AreEqual(root.Collection.First(), returnValue);
}
[TestMethod]
public void CollectionIsNotEmpty_ExtensionMethod_ReturnsFirstToMatchFilter()
{
var defaultValue = "foo";
// this test case is for constant in filter
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.FirstOrDefault(s => s == "blah"), defaultValue);
var root = new Class() { Collection = new string[] { "bar", "blah" } };
var returnValue = lambda(root);
Assert.AreEqual("blah", returnValue);
}
[TestMethod]
public void CollectionIsNotEmpty_ExtensionMethodClosure_ReturnsFirstToMatchFilter()
{
var defaultValue = "foo";
var closureVariable = "blah";
var lambda = Coalesce.CreateDelegate<Class, string>(o => o.Collection.FirstOrDefault(s => s == closureVariable), defaultValue);
var root = new Class() { Collection = new string[] { "bar", "blah" } };
var returnValue = lambda(root);
Assert.AreEqual(closureVariable, returnValue);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment