【问题标题】:How to write left outer join using MethodCallExpressions?如何使用 MethodCallExpression 编写左外连接?
【发布时间】:2013-02-24 02:06:45
【问题描述】:

下面的代码块回答了这个问题:“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 将字符串转换为 lambda (another example) 编写的。如果可能,我希望避免依赖该库。

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

我知道如何使用 ParameterExpressions MemberExpressions 和 Expression.Lambda() 构造基本的 lambda 表达式,例如 foo => foo.Foo_Id,但是如何构造 (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))); 

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

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

根据下面在 LinqPad 中工作的查询,需要重复的过程只需要手动处理 Expression 对象中的透明标识符。查询 sorts 返回按 Bars 排序的 Foos(在本例中为 3 个 Bars)。

附注。这个过程在 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);

【问题讨论】:

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


    【解决方案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>>();
    

    【讨论】:

    • 谢谢杰夫。我在我的问题中添加了一些细节来解释为什么到目前为止我还没有预测到一个定义的类型,以及我最终想要完成的事情。我不熟悉您在 dummyType 中实现的属性“默认”的使用。我会试试看。你认为这可以使用像 Dummy 这样的定义类型来实现吗?非常感谢您的观点和反馈。
    • 与其使用您自己定义的类型,或许您可以尝试使用元组?只要您使用构造函数来创建它们(而不是 Tuple.Create() 工厂),我认为您的 LINQ 提供程序将支持它。只要虚拟类型是仅保存值的简单对象,它们就可以工作。添加任何其他方法等将不起作用,因为大多数(如果不是全部)提供商将不支持它。
    • 我读过关于元组的文章,但从未使用过。会投射到tuple.Item1 = g, tuple.Item2 = v ???如果这是正确的,我将如何在后续项目和 OrderBy 子句中访问它们?
    猜你喜欢
    • 1970-01-01
    • 2014-07-29
    • 2016-06-14
    • 2019-06-17
    • 2014-09-29
    • 2016-11-23
    • 1970-01-01
    • 2011-08-17
    • 1970-01-01
    相关资源
    最近更新 更多