Dynamic linq Building Expression

16,832

Solution 1

You can use the method described here.

You would need to cast the result of the method to Expression<Func<T,bool>>. T being your type.

I will provide an complete example when i get home.

Edit:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Collections;
using System.Reflection;

namespace ExpressionPredicateBuilder
{
    public enum OperatorComparer
    {
        Contains,
        StartsWith,
        EndsWith,
        Equals = ExpressionType.Equal,
        GreaterThan = ExpressionType.GreaterThan,
        GreaterThanOrEqual = ExpressionType.GreaterThanOrEqual,
        LessThan = ExpressionType.LessThan,
        LessThanOrEqual = ExpressionType.LessThan,
        NotEqual = ExpressionType.NotEqual        
    }

public class ExpressionBuilder
{
    public static Expression<Func<T,bool>> BuildPredicate<T>(object value, OperatorComparer comparer, params string[] properties)
    {
        var parameterExpression = Expression.Parameter(typeof(T), typeof(T).Name);
        return (Expression<Func<T, bool>>)BuildNavigationExpression(parameterExpression, comparer, value, properties);
    }

    private static Expression BuildNavigationExpression(Expression parameter, OperatorComparer comparer, object value, params string[] properties)
    {
        Expression resultExpression = null;
        Expression childParameter, predicate;
        Type childType = null;

        if (properties.Count() > 1)
        {
            //build path
            parameter = Expression.Property(parameter, properties[0]);
            var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
            //if it´s a collection we later need to use the predicate in the methodexpressioncall
            if (isCollection)
            {
                childType = parameter.Type.GetGenericArguments()[0];
                childParameter = Expression.Parameter(childType, childType.Name);
            }
            else
            {
                childParameter = parameter;
            }
            //skip current property and get navigation property expression recursivly
            var innerProperties = properties.Skip(1).ToArray();
            predicate = BuildNavigationExpression(childParameter, comparer, value, innerProperties);
            if (isCollection)
            {
                //build subquery
                resultExpression = BuildSubQuery(parameter, childType, predicate);
            }
            else
            {
                resultExpression = predicate;
            }
        }
        else
        {
            //build final predicate
            resultExpression = BuildCondition(parameter, properties[0], comparer, value);
        }
        return resultExpression;
    }

    private static Expression BuildSubQuery(Expression parameter, Type childType, Expression predicate)
    {
        var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
        anyMethod = anyMethod.MakeGenericMethod(childType);
        predicate = Expression.Call(anyMethod, parameter, predicate);
        return MakeLambda(parameter, predicate);
    }

    private static Expression BuildCondition(Expression parameter, string property, OperatorComparer comparer, object value)
    {
        var childProperty = parameter.Type.GetProperty(property);
        var left = Expression.Property(parameter, childProperty);
        var right = Expression.Constant(value);
        var predicate = BuildComparsion(left, comparer, right);
        return MakeLambda(parameter, predicate);
    }

    private static Expression BuildComparsion(Expression left, OperatorComparer comparer, Expression right)
    {
        var mask = new List<OperatorComparer>{
            OperatorComparer.Contains,
            OperatorComparer.StartsWith,
            OperatorComparer.EndsWith
        };
        if(mask.Contains(comparer) && left.Type != typeof(string))
        {
            comparer = OperatorComparer.Equals;
        }
        if(!mask.Contains(comparer))
        {
            return Expression.MakeBinary((ExpressionType)comparer, left, Expression.Convert(right,left.Type));
        }
        return BuildStringCondition(left, comparer, right);            
    }

    private static Expression BuildStringCondition(Expression left, OperatorComparer comparer, Expression right)
    {
        var compareMethod = typeof(string).GetMethods().Single(m => m.Name.Equals(Enum.GetName(typeof(OperatorComparer), comparer)) && m.GetParameters().Count() == 1);
        //we assume ignoreCase, so call ToLower on paramter and memberexpression
        var toLowerMethod = typeof(string).GetMethods().Single(m => m.Name.Equals("ToLower") && m.GetParameters().Count() == 0);
        left = Expression.Call(left, toLowerMethod);
        right = Expression.Call(right, toLowerMethod);
        return Expression.Call(left, compareMethod, right);
    } 

    private static Expression MakeLambda(Expression parameter, Expression predicate)
    {
        var resultParameterVisitor = new ParameterVisitor();
        resultParameterVisitor.Visit(parameter);
        var resultParameter = resultParameterVisitor.Parameter;
        return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
    }

    private class ParameterVisitor : ExpressionVisitor
    {
        public Expression Parameter
        {
            get;
            private set;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            Parameter = node;
            return node;
        }
    }
}

}

This can be used like

var predicate = ExpressionBuilder.BuildPredicate<Books>("Heading",OperatorComparer.Equals,"Page","Heading");
query = query.Where(predicate);

Solution 2

Based on your description I'm not sure that you need Expression. Creating an Expression with a complex object model is quite difficult. Do you really need to create dynamic expression or you simply need to create a dynamic query? If the object model is fixed then you don't need Expression.

I suggest first of all to clean your object model:

  • Rename the Books class to Book (this class represents a Book not a list of books)
  • Rename property Page to Pages (this property returns a list of pages)

Now you can write a dynamic where using just LINQ and one or more helper functions one for each property that you need to search. For example to search for Heading you can write:

        private static bool SearchByHeading(Book b, string heading)
        {
            if (string.IsNullOrEmpty(heading))
                return true;
            else
                return b.Pages.Any(p => p.Heading == heading);
        }

Here you can also see why your previous code didn't work. The expression to search for a given Heading is book.Pages.Any(p => p.Heading == x) and not book.Pages.Heading == x.

In any case given one or more functions like this you can rewrite your code with something like:

using System.Collections.Generic;
using System.Linq;

namespace XMLStorageAndFilter
{
    public class Book
    {
        public Book()
        {
            Pages = new List<Page>();
        }
        public string Title { get; set; }
        public Author Author { get; set; }
        public List<Page> Pages { get; set; }
    }
    public class Author
    {
        public string FirstName { get; set; }
    }
    public class Page
    {
        public string Heading { get; set; }
    }

    public class Program2
    {
        static void Main()
        {
            Page page = new Page();
            page.Heading = "Heading1";
            Book bok = new Book();
            bok.Title = "Title1";
            bok.Author = new Author() { FirstName = "FirstName1" };
            bok.Pages.Add(page);
            List<Book> testList = new List<Book>();
            testList.Add(bok);

            var searchResult = Search(testList, 
                title: "Title1",
                author: "FirstName1",
                heading: "Heading1");
        }

        private static IEnumerable<Book> Search(IEnumerable<Book> books, string author = null, string title = null, string heading = null)
        {
            return books
                .Where((b) => SearchByAuthor(b, author))
                .Where((b) => SearchByHeading(b, heading))
                .Where((b) => SearchByTitle(b, title))
                .ToList();
        }

        private static bool SearchByAuthor(Book b, string author)
        {
            if (string.IsNullOrEmpty(author))
                return true;
            else
                return b.Author.FirstName == author;
        }

        private static bool SearchByTitle(Book b, string title)
        {
            if (string.IsNullOrEmpty(title))
                return true;
            else
                return b.Title == title;
        }

        private static bool SearchByHeading(Book b, string heading)
        {
            if (string.IsNullOrEmpty(heading))
                return true;
            else
                return b.Pages.Any(p => p.Heading == heading);
        }
    }
}

I have skipped search values when null or empty, just an example. This code has also the advantage that is verified at compile time.

Solution 3

Update

The way to query a collection has been answered in the following:

Building a dynamic expression tree to filter on a collection property

Original Response

I believe Davide Lcardi is correct with his statement:

Heading is book.Pages.Any(p => p.Heading == x) and not book.Pages.Heading == x.

if you want to query the list you need to use Any() method to do so. I could not get it exactly right but it should look something like the following using Expression.Call:

                ParameterExpression pe41 = Expression.Parameter(typeof (Page), "pg");
                Expression left41 = Expression.Property(pe41, "Heading");
                Expression right41 = Expression.Constant("Heading");
                Expression e41 = Expression.Equal(left41, right41);

                var methodCall = Expression.Call( Expression.Property(pe11, "Pages"), "Any", new Type[] {typeof(Page), typeof(Boolean)}, e41 );

I am getting this error: No method 'Any' exists on type 'System.Collections.Generic.List`1[SO.Page]'. SO is my NameSpace where class Page exists.

I think I am not sending the correct Type or the whole call may be incorrect. I think this is the correct direction thought to help you find your solution.

Here are some examples I was looking at:

http://msdn.microsoft.com/en-us/library/bb349020(v=vs.110).aspx

http://msdn.microsoft.com/en-us/library/dd402755(v=vs.110).aspx

http://community.bartdesmet.net/blogs/bart/archive/2009/08/10/expression-trees-take-two-introducing-system-linq-expressions-v4-0.aspx

http://blogs.msdn.com/b/csharpfaq/archive/2009/09/14/generating-dynamic-methods-with-expression-trees-in-visual-studio-2010.aspx

Share:
16,832
Binod
Author by

Binod

Updated on June 27, 2022

Comments

  • Binod
    Binod almost 2 years

    I need to create a dynamic linq expression for a dynamic search.The basic search is working but it fails to work with collection. I am able to get the book's title and author but fails to get the required page heading. I get the exception in line "left11 = Expression.Property(page1, "Heading");". I think the expression that i built is unable to recognise the List. How could this be possible? Please see the below code and stacktrace exception.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace XMLStorageAndFilter
    {
    public class Books
    {
        public Books()
        {
            Page = new List<Page>();
        }
        public string Title { get; set; }
        public Author Author { get; set; }
        public List<Page> Page { get; set; }
    }
    public class Author
    {
        public string FirstName { get; set; }
    }
    public class Page
    {
        public string Heading { get; set; }
    }
    
    public class Program2
    {
        static void Main()
        {
            Page page = new Page();
            page.Heading = "Heading";
            Books bok = new Books();
            bok.Title = "Title";
            bok.Author = new Author() { FirstName = "FirstName" };
            bok.Page.Add(page);
            List<Books> testList = new List<Books>();
            testList.Add(bok);
    
            IQueryable<Books> queryableTestData = testList.AsQueryable<Books>();
            ParameterExpression pe11 = Expression.Parameter(typeof(Books), "p");
    
            Expression left11 = Expression.Property(pe11, "Title");
            Expression right11 = Expression.Constant("Title");
            Expression e11 = Expression.Equal(left11, right11);
    
            var author = Expression.Property(pe11, "Author");
            left11 = Expression.Property(author, "FirstName");
            right11 = Expression.Constant("FirstName");
            Expression e21 = Expression.Equal(left11, right11);
    
            Expression predicateBody11 = Expression.And(e11, e21);
            Expression<Func<Books, bool>> condition = Expression.Lambda
                      <Func<Books, bool>>(predicateBody11, new ParameterExpression[] { pe11 });
            var q = queryableTestData.Where(condition);
    
    
            var page1 = Expression.Property(pe11, "Page");
            left11 = Expression.Property(page1, "Heading");
            right11 = Expression.Constant("Heading");
            Expression e22 = Expression.Equal(left11, right11);
    
            Expression predicateBody12 = Expression.And(e11, e22);
    
            Expression<Func<Books, bool>> condition2 = Expression.Lambda
                        <Func<Books, bool>>(predicateBody12, new ParameterExpression[] { pe11 });
    
            var qq1 = queryableTestData.Where(condition2);
        }
    }
    }
    

    Exception Message:- {"Instance property 'Heading' is not defined for type >'System.Collections.Generic.List`1[XMLStorageAndFilter.Page]'"}

    StackTrace:-
    at System.Linq.Expressions.Expression.Property(Expression expression, String propertyName)
    at XMLStorageAndFilter.Program2.Main() in c:\Users\Administrator\Documents\Visual Studio 2013\Projects\XMLStorageAndFilter\NavProperty.cs:line 61
    at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
    at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
    at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
    at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    at System.Threading.ThreadHelper.ThreadStart()

  • Binod
    Binod almost 10 years
    Expression e22 = Expression.Equal(left11, right11); We cannot use the contains instead of Equal here.
  • Binod
    Binod almost 10 years
    I am very thankful for your time. but this is only the sample program. In real scenario the Book class contains many fields (10 -20) and Page class also contains many fields(15 -20) so it wont be possible to write method for each and every field and their combination(and or ..etc). I want to provide the search facility with using any fields. The fields are known only at run time.
  • Sudhir
    Sudhir almost 8 years
    Very well written piece of code. I customized it a bit to chain expressions with OrElse after reading in a space delimited filter string for the application I am working on. I am glad I saw this. Thanks a ton!
  • Cubelaster
    Cubelaster almost 4 years
    I had to add a line of code for BuildStringCondition: m.GetParameters().Any(p => p.ParameterType == typeof(string)) Because Contains has a Char and String overloads