InvocationExpression class allows invocation of other lambda expressions that are parts of the same Expression tree.
You create them with static Expression.Invoke
method.
Problem We want to get on the items which have "car" in their description. We need to check it for null before searching for a string inside but we don't want it to be called excessively, as the computation could be expensive.
using System;
using System.Linq;
using System.Linq.Expressions;
public class Program
{
public static void Main()
{
var elements = new[] {
new Element { Description = "car" },
new Element { Description = "cargo" },
new Element { Description = "wheel" },
new Element { Description = null },
new Element { Description = "Madagascar" },
};
var elementIsInterestingExpression = CreateSearchPredicate(
searchTerm: "car",
whereToSearch: (Element e) => e.Description);
Console.WriteLine(elementIsInterestingExpression.ToString());
var elementIsInteresting = elementIsInterestingExpression.Compile();
var interestingElements = elements.Where(elementIsInteresting);
foreach (var e in interestingElements)
{
Console.WriteLine(e.Description);
}
var countExpensiveComputations = 0;
Action incCount = () => countExpensiveComputations++;
elements
.Where(
CreateSearchPredicate(
"car",
(Element e) => ExpensivelyComputed(
e, incCount
)
).Compile()
)
.Count();
Console.WriteLine("Property extractor is called {0} times.", countExpensiveComputations);
}
private class Element
{
public string Description { get; set; }
}
private static string ExpensivelyComputed(Element source, Action count)
{
count();
return source.Description;
}
private static Expression<Func<T, bool>> CreateSearchPredicate<T>(
string searchTerm,
Expression<Func<T, string>> whereToSearch)
{
var extracted = Expression.Parameter(typeof(string), "extracted");
Expression<Func<string, bool>> coalesceNullCheckWithSearch =
Expression.Lambda<Func<string, bool>>(
Expression.AndAlso(
Expression.Not(
Expression.Call(typeof(string), "IsNullOrEmpty", null, extracted)
),
Expression.Call(extracted, "Contains", null, Expression.Constant(searchTerm))
),
extracted);
var elementParameter = Expression.Parameter(typeof(T), "element");
return Expression.Lambda<Func<T, bool>>(
Expression.Invoke(
coalesceNullCheckWithSearch,
Expression.Invoke(whereToSearch, elementParameter)
),
elementParameter
);
}
}
Output
element => Invoke(extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element))
car
cargo
Madagascar
Predicate is called 5 times.
First thing to note is how the actual propery access, wrapped in an Invoke:
Invoke(e => e.Description, element)
, and this is the only part that touches e.Description
, and in place of it, extracted
parameter of type string
is passed to the next one:
(Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car"))
Another important thing to note here is AndAlso
. It computes only the left part, if the first part returns 'false'. It's a common mistake to use the bitwise operator 'And' instead of it, which always computes both parts, and would fail with a NullReferenceException in this example.