none
Combining Expressions RRS feed

  • Question

  • I am trying to combine two expressions into one.  My goal is to Create the following using Expression Trees.

     

    var v = from pa in dataContext.Person

     from n in pa.PersonNames

     where n.FirstName.StartsWith("M")

    select n;

    My current thinking is this:  Create two expressions, one for the second from clause and another for the where clause.  Then combine both to get the full expression.

                ParameterExpression name = Expression.Parameter(typeof(PersonName), "PersonName");

                Expression expr = Expression.Call(
                            Expression.Property(name, name.Type.GetProperty("FirstName")),
                            typeof(string).GetMethod("StartsWith",new Type[] { typeof(string) }),
                            new Expression[] { Expression.Constant("J", typeof(string)) });


                ParameterExpression pe = Expression.Parameter(typeof(Person), "p");
                Expression patientProperty = Expression.PropertyOrField(pe, typeof(Person).GetProperty("PersonNames").Name);
                var patientLambda = Expression.Lambda(pe, new ParameterExpression[] { pe });
                var collectionLambda = Expression.Lambda<Func<PersonName, bool>>(expr, new ParameterExpression[] { name });
                var collection = Expression.Invoke(collectionLambda, new List<Expression>() { patientProperty});

    The Invoke call generates the following exception: "Expression of type 'EntitySet`1[PersonName]' cannot be used for parameter of type 'PersonName'"

    If I try changing the 'name' parameter expression to the following:

    ParameterExpression entityName = Expression.Parameter(typeof(EntitySet<PersonName>), "PersonName"); the Expression.Call method blows up since there isn't a property of Firstname off of the EntitySet only an object within the EntitySet.

    Am I way off track?  Any help would be greatly appreciated.

     

     

     

     

    Wednesday, January 28, 2009 3:12 PM

Answers

  • Your query translates to these underlying calls

    dataContext.Person.SelectMany(pa => pa.PersonNames, (pa,n) => new {pa, n})
          .Where(x => x.n.FirstName.StartsWith("M"))
          .Select(x => x.n);

    However, since you never refer to 'pa' after the second from clause the query can be simplified to:

    dataContext.Person.SelectMany(pa => pa.PersonNames)
          .Where(n => n.FirstName.StartsWith("M"))
          .Select(n => n);

    And now, since the Select at the end is simply the identity select, you can get rid of that too.

    dataContext.Person.SelectMany(pa => pa.PersonNames)
          .Where(n => n.FirstName.StartsWith("M"));

    So you are left with 2 method invocations and 2 lambda expression.   You can build up a single expression that represents the entire query as method calls.

    ParameterExpression pa = Expression.Parameter(typeof(Person), "pa");
    var selectManyArg = Expression.Lambda(Expression.FieldOrProperty(pa, "PersonNames"), pa);
    Expression selectManyCall = Expression.Call(typeof(Queryable), "SelectMany", new Type[] {typeof(Person), typeof(PersonName)}, Expression.Constant(dataContext.Person), selectManyArg);

    ParameterExpression pn = Expression.Parameter(typeof(PersonName), "pn");
    Experssion whereArg = Expression.Lambda(
            Expression.Call(Expression.FieldOrProperty(pn, "FirstName"), "StartsWith", null, Expression.Constant("M")),
            pn);
    Expression whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] {typeof(PersonName)}, selectManyCall, whereArg);

    And you can make an IQueryable out of it using your table, since it is an IQueryable itself and gives you access to the provider.

    IQueryable query = ((IQueryable)DataContext.Person).Provider.CreateQuery(whereCall);

     

     


    Wayward LINQ Lacky
    • Marked as answer by Kosy1993 Thursday, January 29, 2009 1:36 PM
    Wednesday, January 28, 2009 4:32 PM
    Moderator
  • OK, I got it to work.  The line is question was this:

    Expression selectManyCall = Expression.Call(
                    typeof(Queryable),
                    "SelectMany",
                    new Type[] { typeof(Person), typeof(PersonName) },
                    Expression.Constant(queryableData), selectManyArg);

    and the runtime error was the following: "No method 'SelectMany' on type 'System.Linq.Queryable' is compatible with the supplied arguments."

    Basically, the second parameter was EntitySet<PersonName> while the SelectMany extension method was looking for IEnumerable<PersonName>. Here is the fix.

    Change this:

    LambdaExpression selectManyArg = Expression.Lambda(Expression.Property(patientExp, "vwPatientNames"), patientExp);

     

    to this:

    LambdaExpression selectManyArg = Expression.Lambda<Func<vwPatient,IEnumerable<vwPersonName>>>(Expression.Property(patientExp, "vwPatientNames"), patientExp);

    By forcing the property to IEnumerable the selectManyArg will now satisfy the SelectMany extension method signature. 

     

    Thank you Matt Warren for your expertise and generosity. 

    • Marked as answer by Kosy1993 Thursday, January 29, 2009 1:36 PM
    Thursday, January 29, 2009 1:35 PM

All replies

  • Your query translates to these underlying calls

    dataContext.Person.SelectMany(pa => pa.PersonNames, (pa,n) => new {pa, n})
          .Where(x => x.n.FirstName.StartsWith("M"))
          .Select(x => x.n);

    However, since you never refer to 'pa' after the second from clause the query can be simplified to:

    dataContext.Person.SelectMany(pa => pa.PersonNames)
          .Where(n => n.FirstName.StartsWith("M"))
          .Select(n => n);

    And now, since the Select at the end is simply the identity select, you can get rid of that too.

    dataContext.Person.SelectMany(pa => pa.PersonNames)
          .Where(n => n.FirstName.StartsWith("M"));

    So you are left with 2 method invocations and 2 lambda expression.   You can build up a single expression that represents the entire query as method calls.

    ParameterExpression pa = Expression.Parameter(typeof(Person), "pa");
    var selectManyArg = Expression.Lambda(Expression.FieldOrProperty(pa, "PersonNames"), pa);
    Expression selectManyCall = Expression.Call(typeof(Queryable), "SelectMany", new Type[] {typeof(Person), typeof(PersonName)}, Expression.Constant(dataContext.Person), selectManyArg);

    ParameterExpression pn = Expression.Parameter(typeof(PersonName), "pn");
    Experssion whereArg = Expression.Lambda(
            Expression.Call(Expression.FieldOrProperty(pn, "FirstName"), "StartsWith", null, Expression.Constant("M")),
            pn);
    Expression whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] {typeof(PersonName)}, selectManyCall, whereArg);

    And you can make an IQueryable out of it using your table, since it is an IQueryable itself and gives you access to the provider.

    IQueryable query = ((IQueryable)DataContext.Person).Provider.CreateQuery(whereCall);

     

     


    Wayward LINQ Lacky
    • Marked as answer by Kosy1993 Thursday, January 29, 2009 1:36 PM
    Wednesday, January 28, 2009 4:32 PM
    Moderator
  • Matt,

     Thank you for a quick response and your comprehensive series of blogs regarding creating an IQueryable Provider.   I have taken your recommendations to code and I am still getting hung  up with the following call:

    Expression selectManyCall = Expression.Call(
                    typeof(Queryable),
                    "SelectMany",
                    new Type[] { typeof(Person), typeof(PersonName) },
                    Expression.Constant(queryableData), selectManyArg);

     

    At runtime I get the following error: "No method 'SelectMany' on type 'System.Linq.Queryable' is compatible with the supplied arguments."

    I suspect it may have to do with the fact that the PersonNames property off of the Datacontext.Person is actually an EntitySet. 

    Full Signature: public EntitySet<PersonName> PersonNames {get;set;}

    Would this make a difference?  If so, I have tried substituting typeof(PersonName) with typeof(EntitySet<PersonName>) and so on.  All the substitutions I make still result in the identical error message at runtime.  Could it be I don't have an appropriate SelectMany Extension Method?

     Thank you.

     

    Wednesday, January 28, 2009 7:33 PM
  • OK, I got it to work.  The line is question was this:

    Expression selectManyCall = Expression.Call(
                    typeof(Queryable),
                    "SelectMany",
                    new Type[] { typeof(Person), typeof(PersonName) },
                    Expression.Constant(queryableData), selectManyArg);

    and the runtime error was the following: "No method 'SelectMany' on type 'System.Linq.Queryable' is compatible with the supplied arguments."

    Basically, the second parameter was EntitySet<PersonName> while the SelectMany extension method was looking for IEnumerable<PersonName>. Here is the fix.

    Change this:

    LambdaExpression selectManyArg = Expression.Lambda(Expression.Property(patientExp, "vwPatientNames"), patientExp);

     

    to this:

    LambdaExpression selectManyArg = Expression.Lambda<Func<vwPatient,IEnumerable<vwPersonName>>>(Expression.Property(patientExp, "vwPatientNames"), patientExp);

    By forcing the property to IEnumerable the selectManyArg will now satisfy the SelectMany extension method signature. 

     

    Thank you Matt Warren for your expertise and generosity. 

    • Marked as answer by Kosy1993 Thursday, January 29, 2009 1:36 PM
    Thursday, January 29, 2009 1:35 PM