For a given search tags filter, the expected outcome is an expression representing entities with All tags in a given list of tags ids.
A Lambda might express this as:
class Tag
{
public long TagId { get; set; }
}
class Taggable
{
ICollection<Tag> Tags { get; set; }
}
...
IEnumerable<long> searchTags = new List<long>() { 1, 2, 3 };
Func<Taggable, bool> filter = taggable => searchTags.All(qtag => taggable.Tags.Any(tag => tag.TagId == qtag));
An attempt to represent this as an expression tree fails:
var tagParam = Expression.Parameter(typeof(Tag), "tag");
var taggableParam = Expression.Parameter(typeof(Taggable), "taggable");
MemberExpression tagsProperty = Expression.Property(taggableParam, "Tags");
ConstantExpression searchTagsConstant = Expression.Constant(searchTags);
var containsCall = Expression.Call(
typeof(Enumerable), "Contains",
new[] { typeof(long) },
searchTagsConstant,
Expression.Property(tagParam, "TagID")
);
var anyCall = Expression.Call(
typeof(Enumerable), "Any",
new[] { typeof(Tag) },
tagsProperty,
Expression.Lambda(containsCall, tagParam)
);
// FAILS HERE
var allCall = Expression.Call(
typeof(Enumerable), "All",
new[] { typeof(long) },
searchTagsConstant,
anyCall
);
No generic method 'All' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.
It was thought it would work as the Enumerable.All<TSource, Func<TSource, bool>>
should be satisfied by the searchTagsConstant
and anyCall
?
t was thought it would work as the
Enumerable.All<TSource, Func<TSource, bool>>
should be satisfied by thesearchTagsConstant
andanyCall
Nope. anyCall
is not lambda expression (Func<TSource, bool>
), only a potential body of such expression.
Let start from your target:
IEnumerable<long> searchTags = new List<long>() { 1, 2, 3 };
Expression<Func<Taggable, bool>> lambda =
taggable => searchTags.All(searchTag => taggable.Tags.Any(tag => tag.TagId == searchTag));
The easiest way to learn how to build expression tree is to create a sample expression at compile time and examine the generated code via some decompiler or the expression tree at runtime via debugger.
Anyway, note that the above expression has 3 lambda parameters, while in your attempt you have only 2. Also there is no Contains
call, not sure why you've put in there.
Building the above expression can be like this:
var taggable = Expression.Parameter(typeof(Taggable), "taggable");
var tag = Expression.Parameter(typeof(Tag), "tag");
var searchTag = Expression.Parameter(typeof(long), "searchTag");
// tag.TagId == searchTag
var anyCondition = Expression.Equal(
Expression.Property(tag, "TagId"),
searchTag);
// taggable.Tags.Any(tag => tag.TagId == searchTag)
var anyCall = Expression.Call(
typeof(Enumerable), nameof(Enumerable.Any), new[] { typeof(Tag) },
Expression.Property(taggable, "Tags"), Expression.Lambda(anyCondition, tag));
// searchTags.All(searchTag => taggable.Tags.Any(tag => tag.TagId == searchTag))
var allCall = Expression.Call(
typeof(Enumerable), nameof(Enumerable.All), new[] { typeof(long) },
Expression.Constant(searchTags), Expression.Lambda(anyCall, searchTag));
// taggable => searchTags.All(searchTag => taggable.Tags.Any(tag => tag.TagId == searchTag))
var lambda = Expression.Lambda(allCall, taggable);