LINQ – 完全外部join

我有一个人的ID和他们的名字,以及一个人的ID和他们的姓名列表。 有的人没有名字,有的没有姓; 我想在两个列表上完成一个完整的外连接。

所以下面的列表:

ID FirstName -- --------- 1 John 2 Sue ID LastName -- -------- 1 Doe 3 Smith 

应该产生:

 ID FirstName LastName -- --------- -------- 1 John Doe 2 Sue 3 Smith 

我是LINQ的新手(所以请原谅我,如果我是跛脚),并发现了很多“LINQ外连接”的解决scheme,看起来都非常相似,但似乎留下了外部连接。

我到目前为止的尝试是这样的:

 private void OuterJoinTest() { List<FirstName> firstNames = new List<FirstName>(); firstNames.Add(new FirstName { ID = 1, Name = "John" }); firstNames.Add(new FirstName { ID = 2, Name = "Sue" }); List<LastName> lastNames = new List<LastName>(); lastNames.Add(new LastName { ID = 1, Name = "Doe" }); lastNames.Add(new LastName { ID = 3, Name = "Smith" }); var outerJoin = from first in firstNames join last in lastNames on first.ID equals last.ID into temp from last in temp.DefaultIfEmpty() select new { id = first != null ? first.ID : last.ID, firstname = first != null ? first.Name : string.Empty, surname = last != null ? last.Name : string.Empty }; } } public class FirstName { public int ID; public string Name; } public class LastName { public int ID; public string Name; } 

但是,这返回:

 ID FirstName LastName -- --------- -------- 1 John Doe 2 Sue 

我究竟做错了什么?

我不知道这是否涵盖了所有的情况,逻辑上看来是正确的。 这个想法是采取一个左外连接和右外连接并将它们组合在一起(因为它应该是)。

 var firstNames = new[] { new { ID = 1, Name = "John" }, new { ID = 2, Name = "Sue" }, }; var lastNames = new[] { new { ID = 1, Name = "Doe" }, new { ID = 3, Name = "Smith" }, }; var leftOuterJoin = from first in firstNames join last in lastNames on first.ID equals last.ID into temp from last in temp.DefaultIfEmpty(new { first.ID, Name = default(string) }) select new { first.ID, FirstName = first.Name, LastName = last.Name, }; var rightOuterJoin = from last in lastNames join first in firstNames on last.ID equals first.ID into temp from first in temp.DefaultIfEmpty(new { last.ID, Name = default(string) }) select new { last.ID, FirstName = first.Name, LastName = last.Name, }; var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin); 

这是因为它是在LINQ到对象的书面作品。 如果LINQ to SQL或其他, DefaultIfEmpty()的重载默认可能不起作用。 那么你必须使用条件运算符来有条件地获取值。

 var leftOuterJoin = from first in firstNames join last in lastNames on first.ID equals last.ID into temp from last in temp.DefaultIfEmpty() select new { first.ID, FirstName = first.Name, LastName = last != null ? last.Name : default(string), }; 

更新1:提供一个真正的通用扩展方法FullOuterJoin
更新2:可选地接受关键字types的自定义IEqualityComparer
更新3 :这个实现最近成为MoreLinq一部分 – 谢谢你们!

编辑已添加FullOuterGroupJoin ( ideone )。 我重用了GetOuter<>实现,使得这个性能比它低一点,但是我的目标是“高层次”的代码,而不是最先进的优化。

http://ideone.com/O36nWc上查看

 static void Main(string[] args) { var ax = new[] { new { id = 1, name = "John" }, new { id = 2, name = "Sue" } }; var bx = new[] { new { id = 1, surname = "Doe" }, new { id = 3, surname = "Smith" } }; ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b}) .ToList().ForEach(Console.WriteLine); } 

打印输出:

 { a = { id = 1, name = John }, b = { id = 1, surname = Doe } } { a = { id = 2, name = Sue }, b = } { a = , b = { id = 3, surname = Smith } } 

您也可以提供默认值: http : //ideone.com/kG4kqO

  ax.FullOuterJoin( bx, a => a.id, b => b.id, (a, b, id) => new { a.name, b.surname }, new { id = -1, name = "(no firstname)" }, new { id = -2, surname = "(no surname)" } ) 

印刷:

 { name = John, surname = Doe } { name = Sue, surname = (no surname) } { name = (no firstname), surname = Smith } 

所用术语的解释:

连接是从关系数据库devise借用的术语:

  • 一个连接会重复元素的次数,因为b中的元素具有对应的键值 (即:如果b是空的,则没有任何元素)。 数据库术语调用这个inner (equi)join
  • 外连接包括b 没有对应元素b 元素 。 (即:如果b是空的,则结果甚至)。 这通常被称为left join
  • 一个完整的外部连接包括a blogging,如果另一个中存在相应的元素 。 (即使结果是空的,也是如此)

在RDBMS中通常不会看到的是组join[1]

  • 一个组连接 ,和上面描述的一样, 但是不是从a对应于多个对应的b元素中重复元素, 而是用相应的键对这些logging进行分组 。 当您希望通过基于共同密钥的“join”logging进行枚举时,这样做通常更方便。

另请参见GroupJoin ,它也包含一些一般的背景说明。


[1] (我相信甲骨文和MSSQL有这个专有的扩展)

完整的代码

这是一个广义的“插入式”扩展类

 internal static class MyExtensions { internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>( this IEnumerable<TA> a, IEnumerable<TB> b, Func<TA, TKey> selectKeyA, Func<TB, TKey> selectKeyB, Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection, IEqualityComparer<TKey> cmp = null) { cmp = cmp?? EqualityComparer<TKey>.Default; var alookup = a.ToLookup(selectKeyA, cmp); var blookup = b.ToLookup(selectKeyB, cmp); var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp); keys.UnionWith(blookup.Select(p => p.Key)); var join = from key in keys let xa = alookup[key] let xb = blookup[key] select projection(xa, xb, key); return join; } internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>( this IEnumerable<TA> a, IEnumerable<TB> b, Func<TA, TKey> selectKeyA, Func<TB, TKey> selectKeyB, Func<TA, TB, TKey, TResult> projection, TA defaultA = default(TA), TB defaultB = default(TB), IEqualityComparer<TKey> cmp = null) { cmp = cmp?? EqualityComparer<TKey>.Default; var alookup = a.ToLookup(selectKeyA, cmp); var blookup = b.ToLookup(selectKeyB, cmp); var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp); keys.UnionWith(blookup.Select(p => p.Key)); var join = from key in keys from xa in alookup[key].DefaultIfEmpty(defaultA) from xb in blookup[key].DefaultIfEmpty(defaultB) select projection(xa, xb, key); return join; } } 

这是一个扩展方法:

 public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector) { var leftOuterJoin = from left in leftItems join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp from right in temp.DefaultIfEmpty() select new { left, right }; var rightOuterJoin = from right in rightItems join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp from left in temp.DefaultIfEmpty() select new { left, right }; var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin); return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right)); } 

正如你所发现的,Linq没有“外连接”构造。 您可以得到的最接近的是使用您所述查询的左外连接。 为此,可以添加连接中未表示的姓氏列表的任何元素:

 outerJoin = outerJoin.Concat(lastNames.Select(l=>new { id = l.ID, firstname = String.Empty, surname = l.Name }).Where(l=>!outerJoin.Any(o=>o.id == l.id))); 

我认为其中大部分都是有问题的,包括被接受的答案,因为它们不能和Linq在IQueryable上一起工作,要么是因为服务器往返太多,数据返回太多,要么执行太多的客户端。

IEnumerable我不喜欢Sehe的答案或类似,因为它有过多的内存使用(一个简单的10000000两个列表testing在我的32GB机器上运行Linqpad内存不足)。

另外,大多数其他人实际上并没有实现一个合适的Full Outer Join,因为他们使用的是一个Rightjoin而不是Concat的Rightjoin,这不仅消除了结果中的重复的内部联接行,原始存在于左侧或右侧数据中的任何适当的重复。

所以这里是我的扩展,它处理所有这些问题,生成SQL和直接在Linq中实现连接一样好,在服务器上执行,速度更快,内存less于Enumerables上的其他内存:

 public static class Ext { public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector) { return from left in leftItems join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp from right in temp.DefaultIfEmpty() select resultSelector(left, right); } public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector) { return from right in rightItems join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp from left in temp.DefaultIfEmpty() select resultSelector(left, right); } public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector) { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector) where TLeft : class { var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l)); return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector((TLeft)null,r)); } public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector) where TLeft : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex; public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>( this IQueryable<TLeft> leftItems, IQueryable<TRight> rightItems, Expression<Func<TLeft, TKey>> leftKeySelector, Expression<Func<TRight, TKey>> rightKeySelector, Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable<TRight>)null }; var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p"); var parmC = Expression.Parameter(typeof(TRight), "c"); var argLeft = Expression.PropertyOrField(parmP, "left"); var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), new[] { parmP, parmC }), sampleAnonLR, (TRight)null, (TResult)null); return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs); } public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>( this IQueryable<TLeft> leftItems, IQueryable<TRight> rightItems, Expression<Func<TLeft, TKey>> leftKeySelector, Expression<Func<TRight, TKey>> rightKeySelector, Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null }; var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p"); var parmC = Expression.Parameter(typeof(TLeft), "c"); var argRight = Expression.PropertyOrField(parmP, "right"); var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), new[] { parmP, parmC }), sampleAnonLR, (TLeft)null, (TResult)null); return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs); } public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>( this IQueryable<TLeft> leftItems, IQueryable<TRight> rightItems, Expression<Func<TLeft, TKey>> leftKeySelector, Expression<Func<TRight, TKey>> rightKeySelector, Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex; public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>( this IQueryable<TLeft> leftItems, IQueryable<TRight> rightItems, Expression<Func<TLeft, TKey>> leftKeySelector, Expression<Func<TRight, TKey>> rightKeySelector, Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null }; var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr"); var argLeft = Expression.Constant(null, typeof(TLeft)); var argRight = Expression.PropertyOrField(parmLgR, "right"); var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), new[] { parmLgR }), sampleAnonLgR, (TResult)null); return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs); } public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>( this IQueryable<TLeft> leftItems, IQueryable<TRight> rightItems, Expression<Func<TLeft, TKey>> leftKeySelector, Expression<Func<TRight, TKey>> rightKeySelector, Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } } 

Right Anti-Semi-Join之间的区别主要是针对Linq的对象或源代码,但是在最终答案的服务器端(SQL)有所不同,删除了不必要的JOIN

使用LinqKit可以改进将Expression<Func<>>合并到一个lambda中的Expression的手工编码,但如果语言/编译器为此添加了一些帮助,那将会很好。 FullOuterJoinDistinctRightOuterJoin函数是包含的完整性,但我没有重新实现FullOuterGroupJoin

我猜测@ sehe的方法更强,但是直到我对它更好理解之前,我发现自己已经跨越了@ MichaelSander的扩展。 我修改它以匹配这里描述的内置的Enumerable.Join()方法的语法和返回types。 我在@ JeffMercado的解决scheme中对@ cadrell0的评论附加了“distinct”后缀。

 public static class MyExtensions { public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> ( this IEnumerable<TLeft> leftItems, IEnumerable<TRight> rightItems, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TResult> resultSelector ) { var leftJoin = from left in leftItems join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp from right in temp.DefaultIfEmpty() select resultSelector(left, right); var rightJoin = from right in rightItems join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp from left in temp.DefaultIfEmpty() select resultSelector(left, right); return leftJoin.Union(rightJoin); } } 

在这个例子中,你可以像这样使用它:

 var test = firstNames .FullJoinDistinct( lastNames, f=> f.ID, j=> j.ID, (f,j)=> new { ID = f == null ? j.ID : f.ID, leftName = f == null ? null : f.Name, rightName = j == null ? null : j.Name } ); 

在将来,随着我学到更多的东西,我有一种感觉,就是因为受欢迎,我会转向@ sehe的逻辑。 但即使如此,我也必须小心,因为我认为在可行的情况下至less有一个与现有“.Join()”方法的语法相匹配的重载非常重要,原因有二:

  1. 方法的一致性有助于节省时间,避免错误,避免意外的行为。
  2. 如果将来有一个开箱即用的“.FullJoin()”方法,我会想象它会尽量保持现有的“.Join()”方法的语法。 如果是这样,那么如果你想迁移到它,你可以简单地重命名你的函数,而不必改变参数或担心不同的返回types打破你的代码。

对于generics,扩展,Func语句和其他function我还是一个新的东西,所以反馈肯定是受欢迎的。

编辑:没有花我很长时间才意识到我的代码有问题。 我在LINQPad中做了一个.Dump(),看着返回types。 这只是IEnumerable,所以我试图匹配它。 但是当我真的做了.Where()或.Select()在我的扩展我得到一个错误:“'系统Collections.IEnumerable'不包含'select'和…”的定义。 所以最后我能匹配.Join()的input语法,但不能返回行为。

编辑:添加“TResult”到函数的返回types。 错过了阅读微软的文章,当然这是有道理的。 有了这个修复程序,现在看来,退货行为毕竟符合我的目标。

在两个input上执行内存中的stream式枚举,并为每一行调用select器。 如果在当前迭代中没有相关性,则其中一个select器参数将为空

例:

  var result = left.FullOuterJoin( right, x=>left.Key, x=>right.Key, (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key }); 
  • 需要IComparer作为关联types,如果未提供,则使用Comparer.Default。

  • 要求将“OrderBy”应用于input枚举

     /// <summary> /// Performs a full outer join on two <see cref="IEnumerable{T}" />. /// </summary> /// <typeparam name="TLeft"></typeparam> /// <typeparam name="TValue"></typeparam> /// <typeparam name="TRight"></typeparam> /// <typeparam name="TResult"></typeparam> /// <param name="left"></param> /// <param name="right"></param> /// <param name="leftKeySelector"></param> /// <param name="rightKeySelector"></param> /// <param name="selector">Expression defining result type</param> /// <param name="keyComparer">A comparer if there is no default for the type</param> /// <returns></returns> [System.Diagnostics.DebuggerStepThrough] public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>( this IEnumerable<TLeft> left, IEnumerable<TRight> right, Func<TLeft, TValue> leftKeySelector, Func<TRight, TValue> rightKeySelector, Func<TLeft, TRight, TResult> selector, IComparer<TValue> keyComparer = null) where TLeft: class where TRight: class where TValue : IComparable { keyComparer = keyComparer ?? Comparer<TValue>.Default; using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator()) using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator()) { var hasLeft = enumLeft.MoveNext(); var hasRight = enumRight.MoveNext(); while (hasLeft || hasRight) { var currentLeft = enumLeft.Current; var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue); var currentRight = enumRight.Current; var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue); int compare = !hasLeft ? 1 : !hasRight ? -1 : keyComparer.Compare(valueLeft, valueRight); switch (compare) { case 0: // The selector matches. An inner join is achieved yield return selector(currentLeft, currentRight); hasLeft = enumLeft.MoveNext(); hasRight = enumRight.MoveNext(); break; case -1: yield return selector(currentLeft, default(TRight)); hasLeft = enumLeft.MoveNext(); break; case 1: yield return selector(default(TLeft), currentRight); hasRight = enumRight.MoveNext(); break; } } } } 

我喜欢sehe的答案,但它不使用延迟执行(input序列急切地被ToLookup调用枚举)。 所以在查看LINQ到对象的.NET源代码之后,我想出了这个:

 public static class LinqExtensions { public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> left, IEnumerable<TRight> right, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TKey, TResult> resultSelector, IEqualityComparer<TKey> comparator = null, TLeft defaultLeft = default(TLeft), TRight defaultRight = default(TRight)) { if (left == null) throw new ArgumentNullException("left"); if (right == null) throw new ArgumentNullException("right"); if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector"); if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector"); if (resultSelector == null) throw new ArgumentNullException("resultSelector"); comparator = comparator ?? EqualityComparer<TKey>.Default; return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight); } internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>( this IEnumerable<TLeft> left, IEnumerable<TRight> right, Func<TLeft, TKey> leftKeySelector, Func<TRight, TKey> rightKeySelector, Func<TLeft, TRight, TKey, TResult> resultSelector, IEqualityComparer<TKey> comparator, TLeft defaultLeft, TRight defaultRight) { var leftLookup = left.ToLookup(leftKeySelector, comparator); var rightLookup = right.ToLookup(rightKeySelector, comparator); var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator); foreach (var key in keys) foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft)) foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight)) yield return resultSelector(leftValue, rightValue, key); } } 

这个实现有以下重要的属性:

  • 延迟执行,枚举输出序列之前不会枚举input序列。
  • 只能枚举每个input序列一次。
  • 保留input序列的顺序,这意味着它将按照左侧序列和右侧(左侧序列中不存在的按键)的顺序产生元组。

这些属性是非常重要的,因为它们是FullOuterJoin的新用户,但是LINQ所期望的经验。

我已经为大概6年前的应用程序编写了这个扩展类,并且在许多解决scheme中一直使用它,没有任何问题。 希望能帮助到你。

 public static class JoinExtensions { public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) where TInner : class where TOuter : class { var innerLookup = inner.ToLookup(innerKeySelector); var outerLookup = outer.ToLookup(outerKeySelector); var innerJoinItems = inner .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem))) .Select(innerItem => resultSelector(null, innerItem)); return outer .SelectMany(outerItem => { var innerItems = innerLookup[outerKeySelector(outerItem)]; return innerItems.Any() ? innerItems : new TInner[] { null }; }, resultSelector) .Concat(innerJoinItems); } 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.GroupJoin( inner, outerKeySelector, innerKeySelector, (o, i) => new { o = o, i = i.DefaultIfEmpty() }) .SelectMany(m => miSelect(inn => resultSelector(mo, inn) )); } public static IEnumerable<TResult> RightJoin<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 inner.GroupJoin( outer, innerKeySelector, outerKeySelector, (i, o) => new { i = i, o = o.DefaultIfEmpty() }) .SelectMany(m => moSelect(outt => resultSelector(outt, mi) )); } } 

I really hate these linq expressions, this is why SQL exists:

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname from firstnames fn full join lastnames ln on ln.id=fn.id

Create this as sql view in database and import it as entity.

Of course, (distinct) union of left and right joins will make it too, but it is stupid.