Skip to content

Instantly share code, notes, and snippets.

@spraints
Last active December 14, 2015 15:59
Show Gist options
  • Save spraints/5112097 to your computer and use it in GitHub Desktop.
Save spraints/5112097 to your computer and use it in GitHub Desktop.
This morning at code & coffee (http://www.meetup.com/Indianapolis-Code-and-Coffee/), we were talking about how to combine two Linq expressions together, while preserving the parse tree of both. I didn't look too hard to see if there's a built-in way to do this. If there is, I guess this was just a fun programming exercise.
// Given a class...
class Thing
{
public string Value { get; set; }
}
// If we were just using Func, this is what we'd do:
var f = new Func<Thing, string>(x => x.Value);
var g = f.Wrap(s => (s ?? "").Contains("ok"));
g(new Thing { Value = "bad bad" }); // => false
g(new Thing { Value = "yess ok" }); // => true
static Func<A, C> Wrap<A, B, C>(this Func<A, B> inner, this Func<B, C> outer)
{
return new Func<A, C>(x => outer(inner(x)));
}
// But now we're using expressions.
var ef = new Expression<Func<Thing, string>>(x => x.Value);
var eg = ef.Wrap(s => (s ?? "").Contains("ok"));
// So we want this to work:
var g = g.Compile();
g(new Thing { Value = "bad bad" }); // => false
g(new Thing { Value = "yess ok" }); // => true
// And we want this to work:
eg.ToString(); // x => (x.Value ?? "").Contains("ok")
// One way to do this is to hard-code the outer expression tree:
static Expression<Func<A, bool>> Wrap(this Expression<Func<A, string>> inner)
{
var notNull = Expression.Coalesce(inner.Body, Expression.Constant("")); // x ?? ""
var contains = Expression.Call(notNull, typeof(string).GetMethod("Contains"), Expression.Constant("ok")); // (x ?? "").Contains("ok")
return Expression.Lambda<A, bool>(contains, inner.Parameters);
}
// This isn't very satisfying because Wrap can't be reused.
// Note that this may be good enough, but it's not good enough here. :)
// Another way to do it is to merge the two expression trees.
static Expression<Func<A, C>> Wrap(this Expression<Func<A, B>> inner, Expression<Func<B, C>> outer)
{
var wrapper = new ExpressionWrapper(inner.Body, outer.Parameters[0].Name);
var wrapped = wrapper.Visit(outer.Body);
return Expression.Lambda<A, C>(wrapped, inner.Parameters);
}
class ExpressionWrapper : System.Linq.Expressions.ExpressionVisitor
{
string _parameterName;
Expression _parameterReplacement;
public ExpressionWrapper(Expression parameterReplacement, string parameterName)
{
_parameterName = parameterName;
_parameterReplacement = parameterReplacement;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if(node.Name == _parameterName)
{
return _parameterReplacement;
}
else
{
return super.VisitParameter(node);
}
}
}
// Now we're good:
var ef = new Expression<Func<Thing, string>>(x => x.Value);
var eg = ef.Wrap(s => (s ?? "").Contains("ok"));
eg.ToString(); // x => (x.Value ?? "").Contains("ok")
// Now, let's say that "ok" isn't constant, but rather a variable or something.
var q = "ok";
var ef = new Expression<Func<Thing, string>>(thing => thing.Value);
var eg = f.Wrap(x => (x ?? "").Contains(q));
// Does it work?
var g = eg.Compile();
g(new Thing { Value = "bad bad" }); // => false
g(new Thing { Value = "yess ok" }); // => true
// Yes!
// Does this work?
eg.ToString(); // => x => (x.Value ?? "").Contains(value(YourNamespace.YourClass+<>c__DisplayClass0).q)
// dumb. Maybe you don't care, but let's see if we can replace the DisplayClass.q thing with "ok".
// Add this to our ExpressionWrapper class:
partial class ExpressionWrapper
{
protected override VisitMember(MemberExpression node)
{
if(node.Expression.NodeType == ExpressionType.Constant)
{
var obj = ((ConstantExpression) node.Expression).Value;
if(node.Member is PropertyInfo)
{
return Expression.Constant(((PropertyInfo)node.Member).GetValue(obj, new object[0]));
}
else if(node.Member is FieldInfo)
{
return Expression.Constant(((FieldInfo)node.Member).GetValue(obj));
}
else
{
// Not sure what this is, just leave it.
}
}
return super.VisitMember(node);
}
}
// Aaaaaaand....
eg.ToString() // x => (x.Value ?? "").Contains("ok")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment