如何使用MethodCallExpression写左外连接?

时间:2013-02-24 02:06:46

标签: entity-framework lambda left-join expression-trees linq-expressions

下面的代码块回答了问题:“How do you perform a left outer join using linq extension methods?

var qry = Foo.GroupJoin(
      Bar, 
      foo => foo.Foo_Id,
      bar => bar.Foo_Id,
      (x,y) => new { Foo = x, Bars = y })
.SelectMany(
      x => x.Bars.DefaultIfEmpty(),
      (x,y) => new { Foo = x, Bar = y});

如何将此GroupJoin和SelectMany编写为MethodCallExpressions?我发现的All of the examples是使用DynamicExpressions将字符串转换为lambdas(another example)编写的。我希望尽可能避免依赖该库。

上面的查询可以用表达式和相关方法编写吗?

我知道如何使用ParameterExpressions MemberExpressions和Expression.Lambda()构建像foo => foo.Foo_Id这样的基本lambda表达式,但是如何构造(x,y) => new { Foo = x, Bars = y }) ???能够构建必要的参数来创建两个调用吗?

MethodCallExpression groupJoinCall =
         Expression.Call(
           typeof(Queryable),
           "GroupJoin",
           new Type[] { 
                  typeof(Customers), 
                  typeof(Purchases), 
                  outerSelectorLambda.Body.Type, 
                  resultsSelectorLambda.Body.Type 
               },
                           c.Expression,
                           p.Expression,
                           Expression.Quote(outerSelectorLambda),
                           Expression.Quote(innerSelectorLambda),
                           Expression.Quote(resultsSelectorLambda)
                        );
MethodCallExpression selectManyCall =
   Expression.Call(typeof(Queryable), 
    "SelectMany", new Type[] { 
        groupJoinCall.ElementType, 
        resultType, 
        resultsSelectorLambda.Body.Type 
    }, groupJoinCall.Expression, Expression.Quote(lambda), 
    Expression.Quote(resultsSelectorLambda))); 

最终,我需要创建一个可重复的过程,它将连接n Bars到Foo。因为我们有一个垂直数据结构,所以需要左连接查询来返回表示为Bars的内容,以允许用户对Foo进行排序。要求是允许用户按10条排序,但我不希望它们使用超过3条。我尝试编写一个将上面第一个块中的代码链接到最多10次的进程,但是一旦我通过5,Visual Studio 2012开始变慢,大约7个被锁定。

因此,我现在正在尝试编写一个返回selectManyCall的方法,并按照用户请求的次数递归调用自身。

根据以下在LinqPad中工作的查询,需要重复的过程只需要手动处理Expression对象中的透明标识符。查询排序返回按条排序的Foos(在这种情况下为3条)。

旁注。在OrderBy委托中加入此过程非常容易,但是,它生成的查询包括T-SQL“OUTER APPLY”,Oracle不支持该过程。

我很感激有关如何将投影写入匿名类型或任何其他可能有用的开箱即用的想法的想法。谢谢。

var q = Foos
        .GroupJoin (
            Bars, 
            g => g.FooID, 
            sv => sv.FooID, 
            (g, v) => 
                new  
                {
                    g = g, 
                    v = v
                }
        )
        .SelectMany (
            s => s.v.DefaultIfEmpty (), 
            (s, v) => 
                new  
                {
                    s = s, 
                    v = v
                }
        )
        .GroupJoin (
            Bars, 
            g => g.s.g.FooID, 
            sv => sv.FooID, 
            (g, v) => 
                new  
                {
                    g = g, 
                    v = v
                }
        )
        .SelectMany (
            s => s.v.DefaultIfEmpty (), 
            (s, v) => 
                new  
                {
                    s = s, 
                    v = v
                }
        )
        .GroupJoin (
            Bars,  
            g => g.s.g.s.g.FooID, 
            sv => sv.FooID, 
            (g, v) => 
                new  
                {
                    g = g, 
                    v = v
                }
        )
        .SelectMany (
            s => s.v.DefaultIfEmpty (),  
            (s, v) => 
                new  
                {
                    s = s, 
                    v = v
                }
        )
        .OrderBy (a => a.s.g.s.g.v.Text)
        .ThenBy (a => a.s.g.v.Text)
        .ThenByDescending (a => a.v.Date)
        .Select (a => a.s.g.s.g.s.g);

1 个答案:

答案 0 :(得分:1)

如果您无法弄清楚如何生成表达式,您总是可以从编译器获得帮助。你可以做的是声明一个lambda表达式,其中包含你要查询的类型并编写lambda。编译器将为您生成表达式,您可以检查它以查看构成表达式树的表达式。

例如,您的表达式使用查询语法等效于此(或者如果您愿意,可以使用方法调用语法)

Expression<Func<IQueryable<Foo>, IQueryable<Bar>, IQueryable>> expr =
    (Foo, Bar) =>
        from foo in Foo
        join bar in Bar on foo.Foo_Id equals bar.Foo_Id into bars
        from bar in bars.DefaultIfEmpty()
        select new
        {
            Foo = foo,
            Bar = bar,
        };

要回答您的问题,您无法真正生成创建匿名对象的表达式,在编译时不知道实际类型。你可以通过创建一个虚拟对象来欺骗,并使用GetType()来获取它可以用来创建适当的新表达式的类型,但这更像是一个肮脏的黑客,我不建议这样做。这样做,您将无法生成强类型表达式,因为您不知道匿名类型的类型。

如,

var dummyType = new
{
    foo = default(Foo),
    bars = default(IQueryable<Bar>),
}.GetType();

var fooExpr = Expression.Parameter(typeof(Foo), "foo");
var barsExpr = Expression.Parameter(typeof(IQueryable<Bar>), "bars");
var fooProp = dummyType.GetProperty("foo");
var barsProp = dummyType.GetProperty("bars");
var ctor = dummyType.GetConstructor(new Type[]
{
    fooProp.PropertyType,
    barsProp.PropertyType,
});
var newExpr = Expression.New(
    ctor,
    new Expression[] { fooExpr, barsExpr },
    new MemberInfo[] { fooProp, barsProp }
);
// the expression type is unknown, just some lambda
var lambda = Expression.Lambda(newExpr, fooExpr, barsExpr);

每当您需要生成涉及匿名对象的表达式时,正确的做法是创建一个已知类型并使用它来代替匿名类型。它的使用是有限的,但它是一种更清洁的方式来处理这种情况。那么至少你可以在编译时获得类型。

// use this type instead of the anonymous one
public class Dummy
{
    public Foo foo { get; set; }
    public IQueryable<Bar> bars { get; set; }
}
var dummyType = typeof(Dummy);

var fooExpr = Expression.Parameter(typeof(Foo), "foo");
var barsExpr = Expression.Parameter(typeof(IQueryable<Bar>), "bars");
var fooProp = dummyType.GetProperty("foo");
var barsProp = dummyType.GetProperty("bars");
var ctor = dummyType.GetConstructor(Type.EmptyTypes);

var newExpr = Expression.MemberInit(
    Expression.New(ctor),
    Expression.Bind(fooProp, fooExpr),
    Expression.Bind(barsProp, barsExpr)
);
// lambda's type is known at compile time now
var lambda = Expression.Lambda<Func<Foo, IQueryable<Bar>, Dummy>>(
    newExpr,
    fooExpr,
    barsExpr);

或者,您可以在表达式中使用元组,而不是创建和使用虚拟类型。

static Expression<Func<T1, T2, Tuple<T1, T2>>> GetExpression<T1, T2>()
{
    var type1 = typeof(T1);
    var type2 = typeof(T2);
    var tupleType = typeof(Tuple<T1, T2>);

    var arg1Expr = Expression.Parameter(type1, "arg1");
    var arg2Expr = Expression.Parameter(type2, "arg2");
    var arg1Prop = tupleType.GetProperty("Item1");
    var arg2Prop = tupleType.GetProperty("Item2");
    var ctor = tupleType.GetConstructor(new Type[]
    {
        arg1Prop.PropertyType,
        arg2Prop.PropertyType,
    });

    var newExpr = Expression.New(
        ctor,
        new Expression[] { arg1Expr, arg2Expr },
        new MemberInfo[] { arg1Prop, arg2Prop }
    );
    // lambda's type is known at compile time now
    var lambda = Expression.Lambda<Func<T1, T2, Tuple<T1, T2>>>(
        newExpr,
        arg1Expr,
        arg2Expr);
    return lambda;
}

然后使用它:

var expr = GetExpression<Foo, IQueryable<Bar>>();