Search code examples
c#linqexpression-treessubquery

How Do I Write This Nested Query With Expression Trees?


Motivation

I have a LINQ-to-SQL query that I need to write for production code involving generics and dynamic types. With the help of others on SO, I've been able to determine that I need to create the query using expression trees. In order to get familiar with expression trees, I'm trying to write a simpler statically typed version first, but I'm having trouble figuring out the nested query part.

I've been using MSDN as a reference, primarily this article.

Type

This is the type that will be used in the example query. It's a simplified version of a class used to create a grid with a dynamic number of columns from an SQL database.

public class PivotElement {
    public int Key { get; set; }
    public string DataField { get; set; }
    public string ColumnText { get; set; }
}

Resulting Query

This is the query I would like to eventually build, written in extension syntax:

IQueryable<PivotElement> iQ = ...;
var copy = iQ;

// filterColumn and filterValue are strings that were passed in

copy = copy.Where( 
    pe1 => theIQ.Where( 
        pe2 => 
            pe1.Key == pe1.Key && 
            pe2.DataField == filterColumn && 
            pgr2.ColumnText.Contains( filterValue )
    ).Any()
);

This query filters out rows that don't contain the requested text in the requested column before the elements have been pivoted.

So Far

This is what I've got so far. I think it's mostly right, but I'm not sure how to indicate that the inner Where should always be called on theIQ.

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

namespace IQueryableWhereTypeChange {
    class Program {
        static void Main( string[] args ) {
            var list = new List<PivotElement>() {
                new PivotElement() {
                    Key = 1, DataField = "FirstName", ColumnText = "Bob"
                },
                new PivotElement() {
                    Key = 1, DataField = "LastName", ColumnText = "Sanders"
                },
                new PivotElement() {
                    Key = 2, DataField = "FirstName", ColumnText = "Bob"
                },
                new PivotElement() {
                    Key = 2, DataField = "LastName", ColumnText = "Smith"
                },
                new PivotElement() {
                    Key = 3, DataField = "FirstName", ColumnText = "John"
                },
                new PivotElement() {
                    Key = 3, DataField = "LastName", ColumnText = "Smith"
                }
            };


            var theIQ = list.AsQueryable();
            var iQCopy = theIQ;

            var elementType = typeof( PivotElement );
            var delegateType = typeof( Func<PivotElement, bool> );

            var filterColumn = "LastName";
            var filterValue = "Smith";

            // Query is
            // iQCopy = iQCopy.Where( 
            //    pe1 => iQ.Where( 
            //        pe2 => 
            //            pe1.Key == pe1.Key && 
            //            pe2.DataField == filterColumn && 
            //            pgr2.ColumnText.Contains( filterValue )
            //    ).Any()
            //);

            // So all the elements for keys 2 and 3 should be in the
            //  result set, as those keys have a last name of Smith

            // build pe1
            Type elementType = typeof( PivotElement );
            ParameterExpression pe1 = Expression.Parameter( elementType, "pe1" );
            // build pe2
            ParameterExpression pe2 = Expression.Parameter( elementType, "pe2" );

            // pe1.Key
            Expression pe1KeyProp = Expression.Property( 
                pe1, elementType.GetProperty( "Key" )
            );
            // pe2.Key
            Expression pe2KeyProp = Expression.Property( 
                pe2, elementType.GetProperty( "Key" )
            );
            // build pe1.Key == pe2.Key
            Expression keyEquals = Expression.Equal( pe1KeyProp, pe2KeyProp );

            // build pe2.Datafield
            Expression pe2Datafield = Expression.Property( 
                pe2, elementType.GetProperty( "DataField" )
            );
            // build pe2.DataField == filterColumn
            Expression columnExpression = Expression.Constant( filterColumn );
            Expression dataEquals = Expression.Equal( 
                pe2Datafield, columnExpression 
            );

            // build pe2.ColumnText
            Expression pe2ColumnText = Expression.Property( 
                pe2, elementType.GetProperty( "ColumnText" )
            );
            // build pe2.ColumnText.Contains( filterValue )
            Type stringType = typeof(string);
            Expression valueExpression = Expression.Constant( filterValue );
            Expression textContains = Expression.Call( 
                pe2ColumnText, 
                stringType.GetMethod(
                    "Contains", 
                    new Type[] { stringType } ), 
                    new Expression[] { valueExpression }
            );

            // build pe1.Key == pe2.Key &&
            //       pe2.DataField == filterColumn && 
            //       pe2.ColumnText.Contains( filterValue )
            Expression innerCondition = Expression.AndAlso(
                keyEquals, Expression.AndAlso( dataEquals, textContains )
            );

            // build theIQ.Where( pe2 => innerCondition )
            // build theIQ.Where( pe2 => innerCondition ).Any()
            // build iQCopy.Where( pe1 => anyCall )
            // create the final query

            // enumerate results
            foreach( var pe in results ) {
                Console.WriteLine( 
                    "Key: " + pe.Key + 
                    ", DataField: " + pe.DataField + 
                    ", ColumnText: " + pe.ColumnText 
                );
            }
        }
    }

    public class PivotElement {
        public int Key { get; set; }
        public string DataField { get; set; }
        public string ColumnText { get; set; }
    }
}

Solution

  • The missing bit hit me as I was getting that question all formatted. Since I already had it all typed up, I went ahead and posted it. Here are the missing bits, if anyone is interested:

    // build theIQ.Where( pe2 => innerCondition )
    Type queryableType = typeof( Queryable );
    var delegateType = typeof( Func<PivotElement, bool> );
    MethodCallExpression innerWhere = Expression.Call( 
        queryableType, 
        "Where", 
        new Type[] { elementType }, 
        new Expression[] { 
            theIQ.Expression, 
            Expression.Lambda(
                delegateType, innerCondition, new ParameterExpression[] { pe2 } 
            )
        } 
    );
    
    // build theIQ.Where( pe2 => innerWhere ).Any()
    MethodCallExpression anyCall = Expression.Call( 
        queryableType, "Any", new Type[] { elementType }, innerWhere 
    );
    
    // build iQCopy.Where( pe1 => anyCall )
    MethodCallExpression outerWhere = Expression.Call( 
        queryableType, 
        "Where",
        new Type[] { elementType }, 
        new Expression[] { 
            iQCopy.Expression, 
            Expression.Lambda( 
                delegateType, anyCall, new ParameterExpression[] { pe1 } 
            )
        }
    );
    
    // create the final query
    var results = iQCopy.Provider.CreateQuery<PivotElement>( outerWhere );
    

    The theIQ.Expression is what tells it to use query structure from theIQ, iQCopy.Expression is what tells it to use query structure from iQCopy, and the final iQCopy.Provider is what tells it to apply the constructed query to the actual iQCopy instance.