Search code examples
c#linqexpression-trees

Expression Trees with subquery


My objective is to create a subquery expression tree for a dynamic Full Text Search. In SQL it would be the equivalent of

SELECT * 
FROM MV 
WHERE MV.ID IN (SELECT ID  
                FROM MVF 
                WHERE title = "foo" OR Description = "foo")

So the basic idea is to create the FTS subquery, get the ids from that and use those for the In predicate. My issue is with the second part of that.

// Get subquery for FTS tables
ParameterExpression ftsParam = Expression.Parameter(typeof(MVF), "mvfts");
var wphrase = Expression.Constant("foo");
var methodInfo = typeof(string).GetMethod("Equals", new Type[] { typeof(string) });

var ftsID = Expression.Property(ftsParam, "ID");
var ftsTitle = Expression.Property(ftsParam, "Title");
var ftsDescrip = Expression.Property(ftsParam, "Description");

var texp = Expression.Call(ftsTitle, methodInfo, wphrase);
var dexp = Expression.Call(ftsDescrip, methodInfo, wphrase);
var ftsExp = Expression.Or(texp, dexp);


// Now get ids from the above fts resultset
// THE ASSIGNMENT BELOW THROWS
var selectExp = Expression.Call(typeof(IEnumerable<MVF>), "Select", new Type[]
        {
           typeof(long)
        },
        ftsExp,
        Expression.Lambda<Func<MFV, long>>(
            ftsID,
            ftsParam
        )
);

// Now set up MV table reference
ParameterExpression vParam = Expression.Parameter(typeof(MV), "mv");
var mvID = Expression.Property(vParam, "MVID");
var containsInfo = typeof(IEnumerable<long>).GetMethod("Contains", new Type[] { typeof(long) });

// Now combine expression to get those mvs with ids in the result set of fts query
var containsExp = Expression.Call(selectExp, containsInfo, mvID);
return Expression.Lambda<Func<MV, bool>>(containsExp, vParam);

Exception is:

No generic method 'Select' on type 'System.Collections.Generic.IEnumerable`1[MVF]' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.


Solution

  • Both methods needed by the expression in question are static generic extension methods (with the most important being static and generic) of the Enumerable class:

    Enumerable.Select

    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector
    )
    

    Enumerable.Contains

    public static bool Contains<TSource>(
        this IEnumerable<TSource> source,
        TSource value
    )
    

    The most convenient way of "calling" such methods is the following Expression.Call method overload:

    public static MethodCallExpression Call(
        Type type,
        string methodName,
        Type[] typeArguments,
        params Expression[] arguments
    )
    

    The Type type argument is the type of the class defining the method being called (typeof(Enumerable) in this case) and the Type[] typeArguments is the array with the types of the generic type arguments (empty for non generic methods, should be { typeof(TSource), typeof(TResult) } for Select and { typeof(TSource) } for Contains).

    Applying it to your scenario:

    var selectExp = Expression.Call(
        typeof(Enumerable), 
        "Select",
        new { typeof(MFV), typeof(long) },
        ftsExp,
        Expression.Lambda<Func<MFV, long>>(ftsID, ftsParam)
    );
    

    and

    var containsExp = Expression.Call(
        typeof(Enumerable),
        "Contains",
        new [] { typeof(long) },
        selectExp,
        mvID
    );