Last active
December 14, 2015 15:59
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Given a class... | |
class Thing | |
{ | |
public string Value { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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))); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. :) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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