How do I create an expression tree to represent 'String.Contains(term)' in C#?
Something like:
class Foo
{
public string Bar { get; set; }
}
static void Main()
{
var lambda = GetExpression<Foo>("Bar", "abc");
Foo foo = new Foo { Bar = "aabca" };
bool test = lambda.Compile()(foo);
}
static Expression<Func<T, bool>> GetExpression<T>(string propertyName, string propertyValue)
{
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
You might find this helpful.
How to write string.Contains(someText) in expression Tree
You are pretty close, except constructing the call of Contains
does not have a right side:
Expression col = Expression.Property(pe, columnName);
Expression contains = Expression.Call(
pe
, typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string)}) // Make a static field out of this
, Expression.Constant(searchText) // Prepare a shared object before the loop
);
Once you have your call expressions, combine them with OrElse
to produce the body of your lambda. You can do it with loops, or you can use LINQ:
private static readonly MethodInfo Contains = typeof(string)
.GetMethod(nameof(string.Contains), new Type[] { typeof(string)});
public static Expression<Func<Student,bool>> SearchPredicate(IEnumerable<string> properties, string searchText) {
var param = Expression.Parameter(typeof(Student));
var search = Expression.Constant(searchText);
var components = properties
.Select(propName => Expression.Call(Expression.Property(param, propName), Contains, search))
.Cast<Expression>()
.ToList();
// This is the part that you were missing
var body = components
.Skip(1)
.Aggregate(components[0], Expression.OrElse);
return Expression.Lambda<Func<Student, bool>>(body, param);
}
Building Expression Tree for string.Contains
You are almost there, but your parameter expression should be of type T
, not String
, you are also missing the expression that is getting the property from type T
like name.
What you should roughly have is this
val -> Expression.Constant(typeof(string), rule.Field)
parameter -> Expression.Parameter(typeof(T), "p")
property -> Expression.Property(parameter, "PropertyName")
contains -> Expression.Call(property, containsmethod, val)
equals true -> Expression.True or equals, something like that
I am freehanding all of that, so it's likely somewhat different to be valid. The resulting expression should be something like this
p => p.Name.Contains(val)
Expression Tree for Any with Contains
This should do the trick:
public static class FilterExtensions
{
private readonly struct Node
{
public Node(ParameterExpression parameter, Expression body)
{
Parameter = parameter;
Body = body;
}
public ParameterExpression Parameter { get; }
public Expression Body { get; }
}
public static Expression<Func<T, bool>> BuildFilter<T>(string key, string value)
{
if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
var p = Expression.Parameter(typeof(T), "p");
Expression body = p;
var stack = new Stack<Node>();
foreach (string member in key.Split('.'))
{
if (body.Type.IsGenericType)
{
var genericArgs = body.Type.GetGenericArguments();
if (genericArgs.Length == 1 && typeof(IEnumerable<>)
.MakeGenericType(genericArgs)
.IsAssignableFrom(body.Type))
{
stack.Push(new Node(p, body));
p = Expression.Parameter(genericArgs[0], "s" + stack.Count);
body = p;
}
}
body = Expression.PropertyOrField(body, member);
}
var constant = Expression.Constant(value, typeof(string));
body = Expression.Call(body, nameof(string.Contains), null, constant);
while (stack.Count != 0)
{
var childFilter = Expression.Lambda(body, p);
var parent = stack.Pop();
body = Expression.Call(typeof(Enumerable),
nameof(Enumerable.Any),
new[] { p.Type },
parent.Body,
childFilter);
p = parent.Parameter;
}
return Expression.Lambda<Func<T, bool>>(body, p);
}
}
Usage:
IQueryable<Product> productsQuery = _context.Products
.Include(p => p.ProductEANs);
foreach (KeyValuePair<string, string> searchField in searchFields)
{
if (!string.IsNullOrWhiteSpace(searchField.Value))
{
var filter = FilterExtensions.BuildFilter<Product>(searchField.Key, searchField.Value);
productsQuery = productsQuery.Where(filter);
}
}
How to build expression tree for ContainsT
Basically you need to process the Method, Object(expression that represents the instance for instance method calls or null for static method calls) and Arguments(a collection of expressions that represent arguments of the called method) properties of the MethodCallExpression class.
Specifically for Contains
, you need to avoid (or process differently if needed) the string.Contains
method, and also handle static
methods like Enumerable.Contains
as well as instance methods like ICollection<T>.Contains
, List<T>.Contains
etc. In order to get the list values (when possible), you have to find some sort of a constant expression.
Here is a sample:
private bool ParseContainsExpression(MethodCallExpression expression)
{
// The method must be called Contains and must return bool
if (expression.Method.Name != "Contains" || expression.Method.ReturnType != typeof(bool)) return false;
var list = expression.Object;
Expression operand;
if (list == null)
{
// Static method
// Must be Enumerable.Contains(source, item)
if (expression.Method.DeclaringType != typeof(Enumerable) || expression.Arguments.Count != 2) return false;
list = expression.Arguments[0];
operand = expression.Arguments[1];
}
else
{
// Instance method
// Exclude string.Contains
if (list.Type == typeof(string)) return false;
// Must have a single argument
if (expression.Arguments.Count != 1) return false;
operand = expression.Arguments[0];
// The list must be IEnumerable<operand.Type>
if (!typeof(IEnumerable<>).MakeGenericType(operand.Type).IsAssignableFrom(list.Type)) return false;
}
// Try getting the list items
object listValue;
if (list.NodeType == ExpressionType.Constant)
// from constant value
listValue = ((ConstantExpression)list).Value;
else
{
// from constant value property/field
var listMember = list as MemberExpression;
if (listMember == null) return false;
var listOwner = listMember.Expression as ConstantExpression;
if (listOwner == null) return false;
var listProperty = listMember.Member as PropertyInfo;
listValue = listProperty != null ? listProperty.GetValue(listOwner.Value) : ((FieldInfo)listMember.Member).GetValue(listOwner.Value);
}
var listItems = listValue as System.Collections.IEnumerable;
if (listItems == null) return false;
// Do whatever you like with listItems
return true;
}
How can I create an Expression tree that compares properties in a child object?
You just need to get the property of the returned property value:
var child = Expression.Property(parameter, "Child");
var jobNumber = Expression.Property(child, propertyName);
var lambda =
Expression.Lambda<Func<ParentObject, bool>>(
comparisonMethod(jobNumber, Expression.Constant(propertyValue)),
parameter);
IQueryableT.Contains Expression Tree with Expression parameter
Try reusing parameter from selector expression (i.e. exp
) instead of creating a new one for resulting expression :
Expression<Func<T, bool>> lambda =
Expression.Lambda<Func<T, bool>>(a, exp.Parameters[0]);
Related Topics
Why Enums Require an Explicit Cast to Int Type
For Loop Not Returning Expected Value - C# - Blazor
How to Get the Line Number Which Threw Exception
Converting File into Base64String and Back Again
How to Catch SQLserver Timeout Exceptions
Add Two Integers Using Only Bitwise Operators
Async/Await Different Thread Id
Fluent API, Many-To-Many in Entity Framework Core
Selecting a Textbox Item in a Listbox Does Not Change the Selected Item of the Listbox
How to Fix the Memory Leak in Ie Webbrowser Control
Serialize Property, But Do Not Deserialize Property in JSON.Net
How to Get Around the "'" Problem in SQLite and C#
Create SQLite Database and Table
How to Keep a .Net Console App Running
Catch All Unhandled Exceptions in ASP.NET Web API
What Is the Correct Way to Read a Serial Port Using .Net Framework
How to Merge a List of Lists with Same Type of Items to a Single List of Items