2014-02-06 171 views
19

我想实现返回类型为IQueryable的左外连接扩展方法。使用LINQ进行IQueryable左外连接的扩展方法

我写的功能如下

public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
     this IQueryable<TOuter> outer, 
     IQueryable<TInner> inner, 
     Func<TOuter, TKey> outerKeySelector, 
     Func<TInner, TKey> innerKeySelector, 
     Func<TOuter, TInner, TResult> resultSelector) 
{ 
     return 
      from outerItem in outer 
      join innerItem in inner on outerKeySelector(outerItem) 
      equals innerKeySelector(innerItem) into joinedData 
      from r in joinedData.DefaultIfEmpty() 
      select resultSelector(outerItem, r); 
} 

它不能生成查询。原因可能是:我用Func<>而不是Expression<>。我也尝试了Expression<>。这使我上outerKeySelector(outerItem)线,这是outerKeySelector误差是正被用作方法

我发现SO(如here)和CODEPROJECTS一些讨论的变量,但对于IEnumerable类型IQueryable那些工作不。

+0

您正在收到什么确切的错误消息?我在想的方法是'IQueryable'实际上*是一个'IEnumerable',因此适用于IEnumerable的方法也适用于这个实例,您是否尝试过使用适用于IEnumerable的方法,然后简单地将其转换为' IQueryable'通过调用'.AsQueryable()'? – aevitas

+0

不同之处在于,IQueryable由查询提供程序转换为正确的SQL,然后针对数据库执行正确的SQL,此时IEnumerable基于LINQ to Objects。 IQueryable需要表达式树作为参数,IEnumerable可以与代表一起使用。 – MarcinJuraszek

回答

27

介绍

这个问题很有意思。问题是Funcs代表和表达式是trees,它们是完全不同的结构。当你使用你当前的扩展实现时,它会使用循环,并在每个元素的每个步骤上执行你的选择器,它运行良好。但是当我们谈论实体框架和LINQ时,我们需要遍历树来将其转换为SQL查询。所以它比Funcs更“小”一些(但我仍然喜欢表达式),并且存在如下所述的一些问题。

当你想要做左外连接,您可以使用这样的事情(从这里取:How to implement left join in JOIN Extension method

var leftJoin = p.Person.Where(n => n.FirstName.Contains("a")) 
        .GroupJoin(p.PersonInfo, 
           n => n.PersonId, 
           m => m.PersonId, 
           (n, ms) => new { n, ms = ms.DefaultIfEmpty() }) 
        .SelectMany(z => z.ms.Select(m => new { n = z.n, m)); 

这是件好事,但它不是我们所需要的扩展方法。我想你需要的东西是这样的:

using (var db = new Database1Entities("...")) 
{ 
    var my = db.A.LeftOuterJoin2(db.B, a => a.Id, b => b.IdA, 
     (a, b) => new { a, b, hello = "Hello World!" }); 
    // other actions ... 
} 

有创造这样的扩展许多困难的部分:

  • 手动创建复杂的树,编译器将不会帮助我们在这里是需要方法
  • 思考像WhereSelect
  • 匿名类型(!!我们在这里需要CODEGEN ??我希望没有)

步骤

考虑2个简单的表格:A(列:ID,文本)和B(列编号,IDA,文本)。

OUTER JOIN可以在3个步骤来实现:

// group join as usual + use DefaultIfEmpty 
var q1 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA, 
           (a, b) => new { a, groupB = b.DefaultIfEmpty() }); 

// regroup data to associated list a -> b, it is usable already, but it's 
// impossible to use resultSelector on this stage, 
// beacuse of type difference (quite deep problem: some anonymous type != TOuter) 
var q2 = Queryable.SelectMany(q1, x => x.groupB, (a, b) => new { a.a, b }); 

// second regroup to get the right types 
var q3 = Queryable.SelectMany(db.A, 
           a => q2.Where(x => x.a == a).Select(x => x.b), 
           (a, b) => new {a, b}); 

代码

好吧,我不这么好的柜员,这里是他的代码,我有(对不起,我无法格式化它更好,但它的工作原理!):

public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
     this IQueryable<TOuter> outer, 
     IQueryable<TInner> inner, 
     Expression<Func<TOuter, TKey>> outerKeySelector, 
     Expression<Func<TInner, TKey>> innerKeySelector, 
     Expression<Func<TOuter, TInner, TResult>> resultSelector) 
    { 

     // generic methods 
     var selectManies = typeof(Queryable).GetMethods() 
      .Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3) 
      .OrderBy(x=>x.ToString().Length) 
      .ToList(); 
     var selectMany = selectManies.First(); 
     var select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); 
     var where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2); 
     var groupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5); 
     var defaultIfEmpty = typeof(Queryable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1); 

     // need anonymous type here or let's use Tuple 
     // prepares for: 
     // var q2 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA, (a, b) => new { a, groupB = b.DefaultIfEmpty() }); 
     var tuple = typeof(Tuple<,>).MakeGenericType(
      typeof(TOuter), 
      typeof(IQueryable<>).MakeGenericType(
       typeof(TInner) 
       ) 
      ); 
     var paramOuter = Expression.Parameter(typeof(TOuter)); 
     var paramInner = Expression.Parameter(typeof(IEnumerable<TInner>)); 
     var groupJoinExpression = Expression.Call(
      null, 
      groupJoin.MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), tuple), 
      new Expression[] 
       { 
        Expression.Constant(outer), 
        Expression.Constant(inner), 
        outerKeySelector, 
        innerKeySelector, 
        Expression.Lambda(
         Expression.New(
          tuple.GetConstructor(tuple.GetGenericArguments()), 
          new Expression[] 
           { 
            paramOuter, 
            Expression.Call(
             null, 
             defaultIfEmpty.MakeGenericMethod(typeof (TInner)), 
             new Expression[] 
              { 
               Expression.Convert(paramInner, typeof (IQueryable<TInner>)) 
              } 
           ) 
           }, 
          tuple.GetProperties() 
          ), 
         new[] {paramOuter, paramInner} 
       ) 
       } 
      ); 

     // prepares for: 
     // var q3 = Queryable.SelectMany(q2, x => x.groupB, (a, b) => new { a.a, b }); 
     var tuple2 = typeof (Tuple<,>).MakeGenericType(typeof (TOuter), typeof (TInner)); 
     var paramTuple2 = Expression.Parameter(tuple); 
     var paramInner2 = Expression.Parameter(typeof(TInner)); 
     var paramGroup = Expression.Parameter(tuple); 
     var selectMany1Result = Expression.Call(
      null, 
      selectMany.MakeGenericMethod(tuple, typeof (TInner), tuple2), 
      new Expression[] 
       { 
        groupJoinExpression, 
        Expression.Lambda(
         Expression.Convert(Expression.MakeMemberAccess(paramGroup, tuple.GetProperty("Item2")), 
              typeof (IEnumerable<TInner>)), 
         paramGroup 
       ), 
        Expression.Lambda(
         Expression.New(
          tuple2.GetConstructor(tuple2.GetGenericArguments()), 
          new Expression[] 
           { 
            Expression.MakeMemberAccess(paramTuple2, paramTuple2.Type.GetProperty("Item1")), 
            paramInner2 
           }, 
          tuple2.GetProperties() 
          ), 
         new[] 
          { 
           paramTuple2, 
           paramInner2 
          } 
       ) 
       } 
      ); 

     // prepares for final step, combine all expressinos together and invoke: 
     // var q4 = Queryable.SelectMany(db.A, a => q3.Where(x => x.a == a).Select(x => x.b), (a, b) => new { a, b }); 
     var paramTuple3 = Expression.Parameter(tuple2); 
     var paramTuple4 = Expression.Parameter(tuple2); 
     var paramOuter3 = Expression.Parameter(typeof (TOuter)); 
     var selectManyResult2 = selectMany 
      .MakeGenericMethod(
       typeof(TOuter), 
       typeof(TInner), 
       typeof(TResult) 
      ) 
      .Invoke(
       null, 
       new object[] 
        { 
         outer, 
         Expression.Lambda(
          Expression.Convert(
           Expression.Call(
            null, 
            select.MakeGenericMethod(tuple2, typeof(TInner)), 
            new Expression[] 
             { 
              Expression.Call(
               null, 
               where.MakeGenericMethod(tuple2), 
               new Expression[] 
                { 
                 selectMany1Result, 
                 Expression.Lambda( 
                  Expression.Equal(
                   paramOuter3, 
                   Expression.MakeMemberAccess(paramTuple4, paramTuple4.Type.GetProperty("Item1")) 
                  ), 
                  paramTuple4 
                 ) 
                } 
              ), 
              Expression.Lambda(
               Expression.MakeMemberAccess(paramTuple3, paramTuple3.Type.GetProperty("Item2")), 
               paramTuple3 
              ) 
             } 
           ), 
           typeof(IEnumerable<TInner>) 
          ), 
          paramOuter3 
         ), 
         resultSelector 
        } 
      ); 

     return (IQueryable<TResult>)selectManyResult2; 
    } 

使用

又一次的用法:

db.A.LeftOuterJoin2(db.B, a => a.Id, b => b.IdA, 
     (a, b) => new { a, b, hello = "Hello World!" }); 

看着这个,你能想到的是这一切的SQL查询?这可能是巨大的。你猜怎么了?这是相当小的:

SELECT 
1 AS [C1], 
[Extent1].[Id] AS [Id], 
[Extent1].[Text] AS [Text], 
[Join1].[Id1] AS [Id1], 
[Join1].[IdA] AS [IdA], 
[Join1].[Text2] AS [Text2], 
N'Hello World!' AS [C2] 
FROM [A] AS [Extent1] 
INNER JOIN (SELECT [Extent2].[Id] AS [Id2], [Extent2].[Text] AS [Text], [Extent3].[Id] AS [Id1], [Extent3].[IdA] AS [IdA], [Extent3].[Text2] AS [Text2] 
    FROM [A] AS [Extent2] 
    LEFT OUTER JOIN [B] AS [Extent3] ON [Extent2].[Id] = [Extent3].[IdA]) AS [Join1] ON [Extent1].[Id] = [Join1].[Id2] 

希望它有帮助。

+0

这也基本上是LINQ“语言”的全部原因 - 一旦你进入了连接,使用扩展方法做任何事情都是一个巨大的痛苦。 LINQ关键字的方式产生一个更容易阅读的代码(尽管它在窗帘后面做了同样的事情)。 – Luaan

+0

非常感谢! –

+0

我曾经见过的最英雄的答案。 – spender

3

这是我去年创建的.LeftJoin扩展方法,当时我想简化.GroupJoin。我对它有好运。我包含了XML注释,以便您获得完整的智能感知。 IEqualityComparer也有一个重载。希望对你有帮助。

我加入扩展的全套是在这里:https://github.com/jolsa/Extensions/blob/master/ExtensionLib/JoinExtensions.cs

// JoinExtensions: Created 07/12/2014 - Johnny Olsa 

using System.Linq; 

namespace System.Collections.Generic 
{ 
    /// <summary> 
    /// Join Extensions that .NET should have provided? 
    /// </summary> 
    public static class JoinExtensions 
    { 
     /// <summary> 
     /// Correlates the elements of two sequences based on matching keys. A specified 
     /// System.Collections.Generic.IEqualityComparer&lt;T&gt; is used to compare keys. 
     /// </summary> 
     /// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam> 
     /// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam> 
     /// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam> 
     /// <typeparam name="TResult">The type of the result elements.</typeparam> 
     /// <param name="outer">The first sequence to join.</param> 
     /// <param name="inner">The sequence to join to the first sequence.</param> 
     /// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param> 
     /// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param> 
     /// <param name="resultSelector">A function to create a result element from two combined elements.</param> 
     /// <param name="comparer">A System.Collections.Generic.IEqualityComparer&lt;T&gt; to hash and compare keys.</param> 
     /// <returns> 
     /// An System.Collections.Generic.IEnumerable&lt;T&gt; that has elements of type TResult 
     /// that are obtained by performing an left outer join on two sequences. 
     /// </returns> 
     /// <example> 
     /// Example: 
     /// <code> 
     /// class TestClass 
     /// { 
     ///  static int Main() 
     ///  { 
     ///   var strings1 = new string[] { "1", "2", "3", "4", "a" }; 
     ///   var strings2 = new string[] { "1", "2", "3", "16", "A" }; 
     ///    
     ///   var lj = strings1.LeftJoin(
     ///    strings2, 
     ///    a => a, 
     ///    b => b, 
     ///    (a, b) => (a ?? "null") + "-" + (b ?? "null"), 
     ///    StringComparer.OrdinalIgnoreCase) 
     ///    .ToList(); 
     ///  } 
     /// } 
     /// </code> 
     /// </example> 
     public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, 
      IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, 
      Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer) 
     { 
      return outer.GroupJoin(
       inner, 
       outerKeySelector, 
       innerKeySelector, 
       (o, ei) => ei 
        .Select(i => resultSelector(o, i)) 
        .DefaultIfEmpty(resultSelector(o, default(TInner))), comparer) 
        .SelectMany(oi => oi); 
     } 

     /// <summary> 
     /// Correlates the elements of two sequences based on matching keys. The default 
     /// equality comparer is used to compare keys. 
     /// </summary> 
     /// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam> 
     /// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam> 
     /// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam> 
     /// <typeparam name="TResult">The type of the result elements.</typeparam> 
     /// <param name="outer">The first sequence to join.</param> 
     /// <param name="inner">The sequence to join to the first sequence.</param> 
     /// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param> 
     /// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param> 
     /// <param name="resultSelector">A function to create a result element from two combined elements.</param> 
     /// <returns> 
     /// An System.Collections.Generic.IEnumerable&lt;T&gt; that has elements of type TResult 
     /// that are obtained by performing an left outer join on two sequences. 
     /// </returns> 
     /// <example> 
     /// Example: 
     /// <code> 
     /// class TestClass 
     /// { 
     ///  static int Main() 
     ///  { 
     ///   var strings1 = new string[] { "1", "2", "3", "4", "a" }; 
     ///   var strings2 = new string[] { "1", "2", "3", "16", "A" }; 
     ///    
     ///   var lj = strings1.LeftJoin(
     ///    strings2, 
     ///    a => a, 
     ///    b => b, 
     ///    (a, b) => (a ?? "null") + "-" + (b ?? "null")) 
     ///    .ToList(); 
     ///  } 
     /// } 
     /// </code> 
     /// </example> 
     public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, 
      IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, 
      Func<TOuter, TInner, TResult> resultSelector) 
     { 
      return outer.LeftJoin(inner, outerKeySelector, innerKeySelector, resultSelector, default(IEqualityComparer<TKey>)); 
     } 

    } 
} 
+0

我刚刚意识到我的扩展是IEnumerable而不是IQueryable。当我在LINQPad中测试了我的.LeftJoin并查看了SQL时,它拉出了这两个表并在本地执行了.LeftJoin,因此这是需要考虑的事情。就我的目的而言,我总是使用本地数据,所以没关系。我只是想明确说明在这些扩展的服务器上不会发生LEFT JOIN。 – JohnnyIV

+0

......我意识到我并没有彻底阅读特别寻找IQueryable的最初问题,所以很抱歉。只要您不需要在服务器上发生左连接,这些扩展方法仍然很有用。 – JohnnyIV

+0

我使用您的LeftJoin扩展方法,但在将它们链接在一起时会获得空引用异常。 – Justin

5

接受的答案是一个很好的开始来解释一个左外背后的复杂连接。

我发现了三个比较严重的问题呢,尤其是考虑这个扩展方法时,在更复杂的查询使用它(链接正常多个左外连接联接然后汇总/最大/计数/ ...) 复制之前所选的答案放入您的生产环境中,请继续阅读。

考虑从链接SO后,代表几乎所有的左外的LINQ联接完成最初的例子:

var leftJoin = p.Person.Where(n => n.FirstName.Contains("a")) 
        .GroupJoin(p.PersonInfo, 
           n => n.PersonId, 
           m => m.PersonId, 
           (n, ms) => new { n, ms = ms }) 
        .SelectMany(z => z.ms.DefaultIfEmpty(), (n, m) => new { n = n, m)); 
  • 元组作品的使用,但是当这种被用作部分更复杂的查询,EF失败(不能使用构造函数)。为了解决这个问题,您需要动态生成一个新的匿名类(搜索堆栈溢出)或使用无构造函数的类型。我创建了这个

    internal class KeyValuePairHolder<T1, T2> 
    { 
        public T1 Item1 { get; set; } 
        public T2 Item2 { get; set; } 
    } 
    
  • “Queryable.DefaultIfEmpty”方法的用法。在原始和GroupJoin方法中,由编译器选择的正确方法是“Enumerable.DefaultIfEmpty”方法。这在一个简单的查询中没有任何影响,但请注意接受的答案是否有一堆转换(在IQueryable和IEnumerable之间)。这些投射也会在更复杂的查询中造成问题。 可以在Expression中使用“Enumerable.DefaultIfEmpty”方法,EF知道不执行它,而是将它转换为连接。

  • 最后,这是一个更大的问题:有两个选择完成,而原来只有一个选择。您可以阅读代码注释(原因是类型不同(相当深的问题:某些匿名类型!= TOuter))并在SQL (从内部联接(左外部联接b)中选择)中查看原因。 这里的问题是原始SelectMany方法需要在Join方法中创建的对象类型:TOuter的KeyValuePairHolder和Tinner的IEnumerable作为第一个参数,但传递的resultSelector表达式接受简单的TOUTER作为第一个参数。您可以使用ExpressionVisitor重写传递给正确表单的表达式。

    internal class ResultSelectorRewriter<TOuter, TInner, TResult> : ExpressionVisitor 
    { 
        private Expression<Func<TOuter, TInner, TResult>> resultSelector; 
        public Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>> CombinedExpression { get; private set; } 
    
        private ParameterExpression OldTOuterParamExpression; 
        private ParameterExpression OldTInnerParamExpression; 
        private ParameterExpression NewTOuterParamExpression; 
        private ParameterExpression NewTInnerParamExpression; 
    
    
        public ResultSelectorRewriter(Expression<Func<TOuter, TInner, TResult>> resultSelector) 
        { 
         this.resultSelector = resultSelector; 
         this.OldTOuterParamExpression = resultSelector.Parameters[0]; 
         this.OldTInnerParamExpression = resultSelector.Parameters[1]; 
    
         this.NewTOuterParamExpression = Expression.Parameter(typeof(KeyValuePairHolder<TOuter, IEnumerable<TInner>>)); 
         this.NewTInnerParamExpression = Expression.Parameter(typeof(TInner)); 
    
         var newBody = this.Visit(this.resultSelector.Body); 
         var combinedExpression = Expression.Lambda(newBody, new ParameterExpression[] { this.NewTOuterParamExpression, this.NewTInnerParamExpression }); 
         this.CombinedExpression = (Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>>)combinedExpression; 
        } 
    
    
        protected override Expression VisitParameter(ParameterExpression node) 
        { 
         if (node == this.OldTInnerParamExpression) 
          return this.NewTInnerParamExpression; 
         else if (node == this.OldTOuterParamExpression) 
          return Expression.PropertyOrField(this.NewTOuterParamExpression, "Item1"); 
         else 
          throw new InvalidOperationException("What is this sorcery?", new InvalidOperationException("Did not expect a parameter: " + node)); 
    
        } 
    } 
    

使用表达访问者和KeyValuePairHolder避免元组的使用,我更新下面所选择的答案的版本修复了三个问题,是短,并产生更短的SQL:

internal class QueryReflectionMethods 
    { 
     internal static System.Reflection.MethodInfo Enumerable_Select = typeof(Enumerable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); 
     internal static System.Reflection.MethodInfo Enumerable_DefaultIfEmpty = typeof(Enumerable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1); 

     internal static System.Reflection.MethodInfo Queryable_SelectMany = typeof(Queryable).GetMethods().Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3).OrderBy(x => x.ToString().Length).First(); 
     internal static System.Reflection.MethodInfo Queryable_Where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2); 
     internal static System.Reflection.MethodInfo Queryable_GroupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5); 
     internal static System.Reflection.MethodInfo Queryable_Join = typeof(Queryable).GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).First(c => c.Name == "Join"); 
     internal static System.Reflection.MethodInfo Queryable_Select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); 



     public static IQueryable<TResult> CreateLeftOuterJoin<TOuter, TInner, TKey, TResult>(
        IQueryable<TOuter> outer, 
        IQueryable<TInner> inner, 
        Expression<Func<TOuter, TKey>> outerKeySelector, 
        Expression<Func<TInner, TKey>> innerKeySelector, 
        Expression<Func<TOuter, TInner, TResult>> resultSelector) 
     { 

      var keyValuePairHolderWithGroup = typeof(KeyValuePairHolder<,>).MakeGenericType(
       typeof(TOuter), 
       typeof(IEnumerable<>).MakeGenericType(
        typeof(TInner) 
        ) 
       ); 
      var paramOuter = Expression.Parameter(typeof(TOuter)); 
      var paramInner = Expression.Parameter(typeof(IEnumerable<TInner>)); 
      var groupJoin = 
       Queryable_GroupJoin.MakeGenericMethod(typeof(TOuter), typeof(TInner), typeof(TKey), keyValuePairHolderWithGroup) 
       .Invoke(
        "ThisArgumentIsIgnoredForStaticMethods", 
        new object[]{ 
        outer, 
        inner, 
        outerKeySelector, 
        innerKeySelector, 
        Expression.Lambda(
         Expression.MemberInit(
          Expression.New(keyValuePairHolderWithGroup), 
          Expression.Bind(
           keyValuePairHolderWithGroup.GetMember("Item1").Single(), 
           paramOuter 
           ), 
          Expression.Bind(
           keyValuePairHolderWithGroup.GetMember("Item2").Single(), 
           paramInner 
           ) 
          ), 
         paramOuter, 
         paramInner 
         ) 
        } 
       ); 


      var paramGroup = Expression.Parameter(keyValuePairHolderWithGroup); 
      Expression collectionSelector = Expression.Lambda(     
          Expression.Call(
            null, 
            Enumerable_DefaultIfEmpty.MakeGenericMethod(typeof(TInner)), 
            Expression.MakeMemberAccess(paramGroup, keyValuePairHolderWithGroup.GetProperty("Item2"))) 
          , 
          paramGroup 
         ); 

      Expression newResultSelector = new ResultSelectorRewriter<TOuter, TInner, TResult>(resultSelector).CombinedExpression; 


      var selectMany1Result = 
       Queryable_SelectMany.MakeGenericMethod(keyValuePairHolderWithGroup, typeof(TInner), typeof(TResult)) 
       .Invoke(
        "ThisArgumentIsIgnoredForStaticMethods", new object[]{ 
         groupJoin, 
         collectionSelector, 
         newResultSelector 
        } 
       ); 
      return (IQueryable<TResult>)selectMany1Result; 
     } 
    } 
+0

虽然您的方法似乎与EF6一起工作,但我试图在EF Core 2.0中使用您的建议方法,但未能获得结果。我不确定这是否是EF Core 2.0的错误。我已经问过这里的问题:https://stackoverflow.com/questions/46537158/trying-to-implement-a-leftjoin-extension-method-to-work-with-ef-core-2-0 –

3

正如在前面的答案中所述,当您希望将IQueryable转换为SQL时,您需要使用Expression而不是Func,因此必须执行Expression Tree路由。

但是,您无需自己构建Expression树就可以实现相同的结果。诀窍是,您需要参考LinqKit(可通过NuGet获得)并在查询中调用AsExpandable()。这将负责构建基础表达式树(请参阅here)。

下面的示例使用群组加入的SelectManyDefaultIfEmpty()方法:

代码

public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
     this IQueryable<TOuter> outer, 
     IQueryable<TInner> inner, 
     Expression<Func<TOuter, TKey>> outerKeySelector, 
     Expression<Func<TInner, TKey>> innerKeySelector, 
     Expression<Func<TOuter, TInner, TResult>> resultSelector) 
    { 
     return outer 
      .AsExpandable()// Tell LinqKit to convert everything into an expression tree. 
      .GroupJoin(
       inner, 
       outerKeySelector, 
       innerKeySelector, 
       (outerItem, innerItems) => new { outerItem, innerItems }) 
      .SelectMany(
       joinResult => joinResult.innerItems.DefaultIfEmpty(), 
       (joinResult, innerItem) => 
        resultSelector.Invoke(joinResult.outerItem, innerItem)); 
    } 

样本数据

假设我们有下面EF实体和用户地址变量的访问底层DbSet:

public class User 
{ 
    public int Id { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
} 

public class UserAddress 
{ 
    public int UserId { get; set; } 
    public string LastName { get; set; } 
    public string Street { get; set; } 
} 

IQueryable<User> users; 
IQueryable<UserAddress> addresses; 

使用1

让我们通过用户ID加入:

var result = users.LeftOuterJoin(
      addresses, 
      user => user.Id, 
      address => address.UserId, 
      (user, address) => new { user.Id, address.Street }); 

这意味着(使用LinqPad):

SELECT 
[Extent1].[Id] AS [Id],  
[Extent2].[Street] AS [Street] 
FROM [dbo].[Users] AS [Extent1] 
LEFT OUTER JOIN [dbo].[UserAddresses] AS [Extent2] 
ON [Extent1].[Id] = [Extent2].[UserId] 

用法2

现在,让我们使用匿名类型为重点的多个属性加入:

var result = users.LeftOuterJoin(
      addresses, 
      user => new { user.Id, user.LastName }, 
      address => new { Id = address.UserId, address.LastName }, 
      (user, address) => new { user.Id, address.Street }); 

请注意,匿名类型属性必须具有相同的名称,否则你会得到一个语法错误。

这就是为什么我们有编号= address.UserId,而不是仅仅address.UserId

这将被翻译成:

SELECT 
[Extent1].[Id] AS [Id],  
[Extent2].[Street] AS [Street] 
FROM [dbo].[Users] AS [Extent1] 
LEFT OUTER JOIN [dbo].[UserAddresses] AS [Extent2] 
ON ([Extent1].[Id] = [Extent2].[UserId]) AND ([Extent1].[LastName] = [Extent2].[LastName]) 
0

更新我以前的答案。当我发布它时,我没有注意到这个问题是围绕着SQL转化的。此代码适用于本地项目,因此将首先提取对象,然后,然后加入,而不是在服务器上执行外连接。但使用加入扩展我前面贴到处理空值,这里有一个例子:

public class Person 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 
public class EmailAddress 
{ 
    public int Id { get; set; } 
    public Email Email { get; set; } 
} 
public class Email 
{ 
    public string Name { get; set; } 
    public string Address { get; set; } 
} 

public static void Main() 
{ 
    var people = new [] 
    { 
     new Person() { Id = 1, Name = "John" }, 
     new Person() { Id = 2, Name = "Paul" }, 
     new Person() { Id = 3, Name = "George" }, 
     new Person() { Id = 4, Name = "Ringo" } 
    }; 
    var addresses = new[] 
    { 
     new EmailAddress() { Id = 2, Email = new Email() { Name = "Paul", Address = "[email protected]" } }, 
     new EmailAddress() { Id = 3, Email = new Email() { Name = "George", Address = "[email protected]" } }, 
     new EmailAddress() { Id = 4, Email = new Email() { Name = "Ringo", Address = "[email protected]" } } 
    }; 

    var joinedById = people.LeftJoin(addresses, p => p.Id, a => a.Id, (p, a) => new 
    { 
     p.Id, 
     p.Name, 
     a?.Email.Address 
    }).ToList(); 

    Console.WriteLine("\r\nJoined by Id:\r\n"); 
    joinedById.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? "<null>"}")); 

    var joinedByName = people.LeftJoin(addresses, p => p.Name, a => a?.Email.Name, (p, a) => new 
    { 
     p.Id, 
     p.Name, 
     a?.Email.Address 
    }, StringComparer.OrdinalIgnoreCase).ToList(); 

    Console.WriteLine("\r\nJoined by Name:\r\n"); 
    joinedByName.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? "<null>"}")); 

} 
+0

@RaduV对于处理服务器连接有一个很好的解决方案。我尝试过,我喜欢它。我会补充说,如果适用,我更喜欢'IEnumerable '尽可能加入,因为您不限于与数据库兼容的语法。但在服务器上执行内部/外部连接以提高性能并限制要处理的数据量是有益的。 – JohnnyIV

0

@Licentia,这是我想出了解决您的问题。我创建了DynamicJoinDynamicLeftJoin扩展方法,与您向我展示的方法类似,但我处理输出的方式不同,因为字符串解析容易受到许多问题的影响。这不会加入匿名类型,但你可以调整它来做到这一点。它也没有IComparable的过载,但可以很容易地添加。属性名称必须与类型相同。这用于联合与我上面的扩展方法(即没有它们将无法工作)。我希望它有帮助!

public class Person 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 
public class EmailAddress 
{ 
    public int PersonId { get; set; } 
    public Email Email { get; set; } 
} 
public class Email 
{ 
    public string Name { get; set; } 
    public string Address { get; set; } 
} 

public static void Main() 
{ 
    var people = new[] 
    { 
     new Person() { Id = 1, Name = "John" }, 
     new Person() { Id = 2, Name = "Paul" }, 
     new Person() { Id = 3, Name = "George" }, 
     new Person() { Id = 4, Name = "Ringo" } 
    }; 
    var addresses = new[] 
    { 
     new EmailAddress() { PersonId = 2, Email = new Email() { Name = "Paul", Address = "[email protected]" } }, 
     new EmailAddress() { PersonId = 3, Email = new Email() { Name = "George", Address = "[email protected]" } }, 
     new EmailAddress() { PersonId = 4, Email = new Email() { Name = "Ringo" } } 
    }; 

    Console.WriteLine("\r\nInner Join:\r\n"); 
    var innerJoin = people.DynamicJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList(); 
    innerJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? "<null>"}")); 

    Console.WriteLine("\r\nOuter Join:\r\n"); 
    var leftJoin = people.DynamicLeftJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList(); 
    leftJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? "<null>"}")); 

} 

public static class DynamicJoinExtensions 
{ 
    private const string OuterPrefix = "outer."; 
    private const string InnerPrefix = "inner."; 

    private class Processor<TOuter, TInner> 
    { 
     private readonly Type _typeOuter = typeof(TOuter); 
     private readonly Type _typeInner = typeof(TInner); 
     private readonly PropertyInfo _keyOuter; 
     private readonly PropertyInfo _keyInner; 
     private readonly List<string> _outputFields; 
     private readonly Dictionary<string, PropertyInfo> _resultProperties; 

     public Processor(string outerKey, string innerKey, IEnumerable<string> outputFields) 
     { 
      _outputFields = outputFields.ToList(); 

      // Check for properties with the same name 
      string badProps = string.Join(", ", _outputFields.Select(f => new { property = f, name = GetName(f) }) 
       .GroupBy(f => f.name, StringComparer.OrdinalIgnoreCase) 
       .Where(g => g.Count() > 1) 
       .SelectMany(g => g.OrderBy(f => f.name, StringComparer.OrdinalIgnoreCase).Select(f => f.property))); 
      if (!string.IsNullOrEmpty(badProps)) 
       throw new ArgumentException($"One or more {nameof(outputFields)} are duplicated: {badProps}"); 

      _keyOuter = _typeOuter.GetProperty(outerKey); 
      _keyInner = _typeInner.GetProperty(innerKey); 

      // Check for valid keys 
      if (_keyOuter == null || _keyInner == null) 
       throw new ArgumentException($"One or both of the specified keys is not a valid property"); 

      // Check type compatibility 
      if (_keyOuter.PropertyType != _keyInner.PropertyType) 
       throw new ArgumentException($"Keys must be the same type. ({nameof(outerKey)} type: {_keyOuter.PropertyType.Name}, {nameof(innerKey)} type: {_keyInner.PropertyType.Name})"); 

      Func<string, Type, IEnumerable<KeyValuePair<string, PropertyInfo>>> getResultProperties = (prefix, type) => 
       _outputFields.Where(f => f.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) 
        .Select(f => new KeyValuePair<string, PropertyInfo>(f, type.GetProperty(f.Substring(prefix.Length)))); 

      // Combine inner/outer outputFields with PropertyInfo into a dictionary 
      _resultProperties = getResultProperties(OuterPrefix, _typeOuter).Concat(getResultProperties(InnerPrefix, _typeInner)) 
       .ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase); 

      // Check for properties that aren't found 
      badProps = string.Join(", ", _resultProperties.Where(kv => kv.Value == null).Select(kv => kv.Key)); 
      if (!string.IsNullOrEmpty(badProps)) 
       throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}"); 

      // Check for properties that aren't the right format 
      badProps = string.Join(", ", _outputFields.Where(f => !_resultProperties.ContainsKey(f))); 
      if (!string.IsNullOrEmpty(badProps)) 
       throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}"); 

     } 
     // Inner Join 
     public IEnumerable<dynamic> Join(IEnumerable<TOuter> outer, IEnumerable<TInner> inner) => 
      outer.Join(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i)); 
     // Left Outer Join 
     public IEnumerable<dynamic> LeftJoin(IEnumerable<TOuter> outer, IEnumerable<TInner> inner) => 
      outer.LeftJoin(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i)); 

     private static string GetName(string fieldId) => fieldId.Substring(fieldId.IndexOf('.') + 1); 
     private object GetOuterKeyValue(TOuter obj) => _keyOuter.GetValue(obj); 
     private object GetInnerKeyValue(TInner obj) => _keyInner.GetValue(obj); 
     private object GetResultProperyValue(string key, object obj) => _resultProperties[key].GetValue(obj); 
     private dynamic CreateItem(TOuter o, TInner i) 
     { 
      var obj = new ExpandoObject(); 
      var dict = (IDictionary<string, object>)obj; 
      _outputFields.ForEach(f => 
      { 
       var source = f.StartsWith(OuterPrefix, StringComparison.OrdinalIgnoreCase) ? (object)o : i; 
       dict.Add(GetName(f), source == null ? null : GetResultProperyValue(f, source)); 
      }); 
      return obj; 
     } 
    } 

    public static IEnumerable<dynamic> DynamicJoin<TOuter, TInner>(this IEnumerable<TOuter> outer, 
      IEnumerable<TInner> inner, string outerKey, string innerKey, 
      params string[] outputFields) => 
     new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).Join(outer, inner); 
    public static IEnumerable<dynamic> DynamicLeftJoin<TOuter, TInner>(this IEnumerable<TOuter> outer, 
      IEnumerable<TInner> inner, string outerKey, string innerKey, 
      params string[] outputFields) => 
     new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).LeftJoin(outer, inner); 
}