LINQ – Full Outer Join

J’ai une liste de pièces d’identité et leur prénom, ainsi qu’une liste de pièces d’identité et leur nom de famille. Certaines personnes n’ont pas de prénom et d’autres n’ont pas de nom de famille. Je voudrais faire une jointure externe complète sur les deux listes.

Donc les listes suivantes:

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

Devrait produire:

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

Je suis nouveau sur LINQ (alors pardonnez-moi si je suis boiteux) et j’ai trouvé pas mal de solutions pour ‘LINQ Outer Joins’ qui ont toutes l’air assez similaires, mais semblent vraiment être des jointures externes.

Mes tentatives jusqu’ici vont quelque chose comme ceci:

 private void OuterJoinTest() { List firstNames = new List(); firstNames.Add(new FirstName { ID = 1, Name = "John" }); firstNames.Add(new FirstName { ID = 2, Name = "Sue" }); List lastNames = new List(); 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 : ssortingng.Empty, surname = last != null ? last.Name : ssortingng.Empty }; } } public class FirstName { public int ID; public ssortingng Name; } public class LastName { public int ID; public ssortingng Name; } 

Mais cela revient:

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

Qu’est-ce que je fais mal?

Je ne sais pas si cela couvre tous les cas, logiquement cela semble correct. L’idée est de prendre une jointure externe gauche et une jointure externe droite et de les combiner (comme cela devrait être).

 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(ssortingng) }) 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(ssortingng) }) select new { last.ID, FirstName = first.Name, LastName = last.Name, }; var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin); 

Cela fonctionne comme écrit car il est dans LINQ to Objects. Si LINQ to SQL ou autre, la surcharge de DefaultIfEmpty() qui prend par défaut peut ne pas fonctionner. Ensuite, vous devez utiliser l’opérateur conditionnel pour obtenir les valeurs de manière conditionnelle.

c’est à dire,

 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(ssortingng), }; 

Mise à jour 1: fournir une méthode d’extension vraiment généralisée FullOuterJoin
Mise à jour 2: acceptation facultative d’un IEqualityComparer personnalisé pour le type de clé
Mise à jour 3 : cette implémentation a récemment fait partie de MoreLinq – Merci les gars!

Modifier Ajouté FullOuterGroupJoin ( ideone ). J’ai réutilisé l’ GetOuter<> , ce qui en fait une fraction moins performante que cela pourrait être, mais je vise un code de «haut niveau», pas optimisé pour l’instant, pour le moment.

Voir en direct sur http://ideone.com/O36nWc

 static void Main(ssortingng[] 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); } 

Imprime la sortie:

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

Vous pouvez également fournir des valeurs par défaut: 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)" } ) 

Impression:

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

Explication des termes utilisés:

Rejoindre est un terme emprunté à la conception de firebase database relationnelle:

  • Une jointure répète des éléments autant de fois qu’il ya d’éléments dans b avec la clé correspondante (c’est-à-dire: rien si b était vide). Le jargon de firebase database appelle cette inner (equi)join .
  • Une jointure externe comprend des éléments d’un élément pour lesquels aucun élément correspondant n’existe dans b . (ie: même résultats si b étaient vides). Ceci est généralement appelé left join .
  • Une jointure externe complète inclut les enregistrements de a et b si aucun élément correspondant n’existe dans l’autre. (c.-à-d. même des résultats si a était vide)

Quelque chose que l’on ne voit généralement pas dans le SGBDR est une jointure de groupe [1] :

  • Une jointure de groupe fait la même chose que celle décrite ci-dessus, mais au lieu de répéter les éléments d’ a multiple correspondant à b , elle regroupe les enregistrements avec les clés correspondantes. Ceci est souvent plus pratique lorsque vous souhaitez énumérer des enregistrements “joints”, basés sur une clé commune.

Voir aussi GroupJoin qui contient également des explications générales sur le contexte.


[1] (Je pense qu’Oracle et MSSQL ont des extensions propriétaires pour cela)

Code complet

Une classe d’extension ‘drop-in’ généralisée pour cela

 internal static class MyExtensions { internal static IEnumerable FullOuterGroupJoin( this IEnumerable a, IEnumerable b, Func selectKeyA, Func selectKeyB, Func, IEnumerable, TKey, TResult> projection, IEqualityComparer cmp = null) { cmp = cmp?? EqualityComparer.Default; var alookup = a.ToLookup(selectKeyA, cmp); var blookup = b.ToLookup(selectKeyB, cmp); var keys = new HashSet(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 FullOuterJoin( this IEnumerable a, IEnumerable b, Func selectKeyA, Func selectKeyB, Func projection, TA defaultA = default(TA), TB defaultB = default(TB), IEqualityComparer cmp = null) { cmp = cmp?? EqualityComparer.Default; var alookup = a.ToLookup(selectKeyA, cmp); var blookup = b.ToLookup(selectKeyB, cmp); var keys = new HashSet(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; } } 

Je pense qu’il y a des problèmes avec la plupart d’entre eux, y compris la réponse acceptée, car ils ne fonctionnent pas bien avec Linq sur IQueryable, soit en raison d’un trop grand nombre d’allers-retours de serveurs et de retours de données, soit d’une exécution client trop importante.

Pour IEnumerable, je n’aime pas la réponse de Sehe ou similaire, car il utilise trop de mémoire (un simple test à deux listes 10000000 exécutait Linqpad sans mémoire sur mon ordinateur de 32 Go).

En outre, la plupart des autres n’implémentent pas réellement une jointure externe complète car ils utilisent une union avec une jointure droite au lieu de concat avec une jointure anti-semi-droite, ce qui élimine non seulement les lignes de jointure internes du résultat, mais tous les doublons appropriés qui existaient à l’origine dans les données de gauche ou de droite.

Donc, voici mes extensions qui gèrent tous ces problèmes, génèrent du code SQL, implémentent directement la jointure dans Linq, s’exécutent sur le serveur, et sont plus rapides et avec moins de mémoire que d’autres sur Enumerables:

 public static class Ext { public static IEnumerable LeftOuterJoin( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func 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 RightOuterJoin( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func 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 FullOuterJoinDistinct( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func resultSelector) { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } public static IEnumerable RightAntiSemiJoin( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func resultSelector) where TLeft : class { var hashLK = new HashSet(from l in leftItems select leftKeySelector(l)); return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector((TLeft)null,r)); } public static IEnumerable FullOuterJoin( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func resultSelector) where TLeft : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } private static Expression> CastSMBody(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression>)ex; public static IQueryable LeftOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable)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 RightOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLR = new { leftg = (IEnumerable)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 FullOuterJoinDistinct( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> 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> CastSBody(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression>)ex; public static IQueryable RightAntiSemiJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { var sampleAnonLgR = new { leftg = (IEnumerable)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 FullOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } } 

La différence entre un Anti-Semi-Join à droite est principalement liée à Linq to Objects ou à la source, mais fait une différence du côté du serveur (SQL) dans la réponse finale, supprimant un JOIN inutile.

Le codage manuel de l’ Expression pour gérer la fusion d’une Expression> en un lambda pourrait être amélioré avec LinqKit, mais ce serait bien si le langage / compilateur avait ajouté de l’aide pour cela. Les fonctions FullOuterJoinDistinct et RightOuterJoin sont incluses pour être complètes, mais je n’ai pas encore implémenté FullOuterGroupJoin .

J’ai écrit une autre version d’une jointure externe complète pour IEnumerable pour les cas où la clé est ordonnable, ce qui est environ 50% plus rapide que la combinaison de la jointure externe gauche avec la jointure anti semi-droite, au moins sur les petites collections. Il traverse chaque collection après avoir sortingé une seule fois.

Voici une méthode d’extension qui fait cela:

 public static IEnumerable> FullOuterJoin(this IEnumerable leftItems, Func leftIdSelector, IEnumerable rightItems, Func 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(x.left, x.right)); } 

Comme vous l’avez trouvé, Linq n’a pas de construction “join join”. Le plus proche que vous pouvez obtenir est une jointure externe gauche en utilisant la requête que vous avez indiquée. Pour cela, vous pouvez append des éléments de la liste des noms de famille qui ne sont pas représentés dans la jointure:

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

Je devine que l’approche de @ sehe est plus forte, mais jusqu’à ce que je le comprenne mieux, je me retrouve à dépasser l’extension de @ MichaelSander. Je l’ai modifié pour correspondre à la syntaxe et au type de retour de la méthode intégrée Enumerable.Join () décrite ici . J’ai ajouté le suffixe “distinct” en ce qui concerne le commentaire de @ cadrell0 sous la solution de @ JeffMercado.

 public static class MyExtensions { public static IEnumerable FullJoinDistinct ( this IEnumerable leftItems, IEnumerable rightItems, Func leftKeySelector, Func rightKeySelector, Func 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); } } 

Dans l’exemple, vous l’utiliseriez comme ceci:

 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 } ); 

À l’avenir, au fur et à mesure que j’apprendrai, j’ai le sentiment que je migrerai vers la logique de @ sehe étant donné sa popularité. Mais même dans ce cas, je dois faire attention, car j’estime important d’avoir au moins une surcharge correspondant à la syntaxe de la méthode “.Join ()” existante, si possible, pour deux raisons:

  1. La cohérence des méthodes permet de gagner du temps, d’éviter les erreurs et d’éviter les comportements involontaires.
  2. Si jamais il y a une méthode “.FullJoin ()” prête à l’emploi, j’imagine qu’elle essaiera de garder la syntaxe de la méthode “.Join ()” existante si c’est possible. Si c’est le cas, alors, si vous souhaitez y migrer, vous pouvez simplement renommer vos fonctions sans modifier les parameters ou vous soucier des différents types de retour qui violent votre code.

Je suis toujours nouveau en ce qui concerne les génériques, les extensions, les relevés Func et d’autres fonctionnalités.

EDIT: Il ne m’a pas fallu longtemps pour réaliser qu’il y avait un problème avec mon code. Je faisais un .Dump () dans LINQPad et regardais le type de retour. C’était juste IEnumerable, alors j’ai essayé de le faire correspondre. Mais quand j’ai fait un .Where () ou .Select () sur mon extension, j’ai reçu une erreur: “System Collections.IEnumerable” ne contient pas de définition pour “Select” et … “. Donc, au final, j’ai pu faire correspondre la syntaxe d’entrée de .Join (), mais pas le comportement de retour.

EDIT: Ajout de “TResult” au type de retour pour la fonction. Cela manquait lors de la lecture de l’article de Microsoft, et bien sûr, cela a du sens. Avec ce correctif, il semble maintenant que le comportement de retour est conforme à mes objectives après tout.

J’aime la réponse de sehe, mais elle n’utilise pas l’exécution différée (les séquences d’entrée sont énumérées avec enthousiasme par les appels à ToLookup). Donc, après avoir examiné les sources .NET pour les objects LINQ , je suis arrivé à ceci:

 public static class LinqExtensions { public static IEnumerable FullOuterJoin( this IEnumerable left, IEnumerable right, Func leftKeySelector, Func rightKeySelector, Func resultSelector, IEqualityComparer 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.Default; return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight); } internal static IEnumerable FullOuterJoinIterator( this IEnumerable left, IEnumerable right, Func leftKeySelector, Func rightKeySelector, Func resultSelector, IEqualityComparer 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); } } 

Cette implémentation a les propriétés importantes suivantes:

  • L’exécution différée, les séquences d’entrée ne seront pas énumérées avant que la séquence de sortie ne soit énumérée.
  • Énumère seulement les séquences d’entrée une fois chacune.
  • Préserve l’ordre des séquences en entrée, dans le sens où il générera des tuples dans l’ordre de la séquence de gauche puis du droit (pour les clés non présentes dans la séquence de gauche).

Ces propriétés sont importantes, car elles sont attendues par les nouveaux utilisateurs de FullOuterJoin mais expérimentés avec LINQ.

Effectue une énumération en continu en continu sur les deux entrées et appelle le sélecteur pour chaque ligne. S’il n’y a pas de corrélation à l’itération en cours, l’ un des arguments du sélecteur sera nul .

Exemple:

  var result = left.FullOuterJoin( right, x=>left.Key, x=>right.Key, (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key }); 
  • Nécessite un IComparer pour le type de corrélation, utilise le paramètre Comparer.Default s’il n’est pas fourni.

  • Nécessite que ‘OrderBy’ soit appliqué aux énumérables en entrée

     ///  /// Performs a full outer join on two . ///  ///  ///  ///  ///  ///  ///  ///  ///  /// Expression defining result type /// A comparer if there is no default for the type ///  [System.Diagnostics.DebuggerStepThrough] public static IEnumerable FullOuterJoin( this IEnumerable left, IEnumerable right, Func leftKeySelector, Func rightKeySelector, Func selector, IComparer keyComparer = null) where TLeft: class where TRight: class where TValue : IComparable { keyComparer = keyComparer ?? Comparer.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; } } } } 

J’ai décidé d’append ceci comme une réponse séparée car je ne suis pas sûr que cela soit suffisamment testé. Il s’agit d’une ré-implémentation de la méthode FullOuterJoin utilisant essentiellement une version personnalisée simplifiée de LINQKit Invoke / Expand for Expression afin qu’elle fonctionne avec Entity Framework. Il n’y a pas beaucoup d’explication car c’est à peu près la même chose que ma réponse précédente.

 public static class Ext { private static Expression> CastSMBody(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression>)ex; public static IQueryable LeftOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { // (lrg,r) => resultSelector(lrg.left, r) var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable)null }; var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg"); var parmC = Expression.Parameter(typeof(TRight), "r"); var argLeft = Expression.PropertyOrField(parmP, "left"); var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), new[] { parmP, parmC }), sampleAnonLR, (TRight)null, (TResult)null); return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs); } public static IQueryable RightOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { // (lgr,l) => resultSelector(l, lgr.right) var sampleAnonLR = new { leftg = (IEnumerable)null, right = (TRight)null }; var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr"); var parmC = Expression.Parameter(typeof(TLeft), "l"); var argRight = Expression.PropertyOrField(parmP, "right"); var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(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); } private static Expression> CastSBody(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression>)ex; public static IQueryable RightAntiSemiJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { // newrightrs = lgr => resultSelector((TLeft)null, lgr.right) var sampleAnonLgR = new { leftg = (IEnumerable)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(resultSelector.Apply(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 FullOuterJoin( this IQueryable leftItems, IQueryable rightItems, Expression> leftKeySelector, Expression> rightKeySelector, Expression> resultSelector) where TLeft : class where TRight : class where TResult : class { return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector)); } public static Expression Apply(this LambdaExpression e, params Expression[] args) { var b = e.Body; foreach (var pa in e.Parameters.Cast().Zip(args, (p, a) => (p, a))) { b = b.Swap(pa.p, pa.a); } return b.PropagateNull(); } public static Expression Swap(this Expression orig, Expression from, Expression to) => new SwapVisitor(from, to).Visit(orig); public class SwapVisitor : System.Linq.Expressions.ExpressionVisitor { public readonly Expression from; public readonly Expression to; public SwapVisitor(Expression _from, Expression _to) { from = _from; to = _to; } public override Expression Visit(Expression node) => node == from ? to : base.Visit(node); } public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig); public class NullVisitor : System.Linq.Expressions.ExpressionVisitor { public override Expression Visit(Expression node) { if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null) return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType()); else return base.Visit(node); } } public static Type GetMemberType(this MemberInfo member) { switch (member) { case FieldInfo mfi: return mfi.FieldType; case PropertyInfo mpi: return mpi.PropertyType; case EventInfo mei: return mei.EventHandlerType; default: throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member)); } } } 

I’ve written this extensions class for an app perhaps 6 years ago, and have been using it ever since in many solutions without issues. J’espère que cela aide.

 public static class JoinExtensions { public static IEnumerable FullOuterJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func 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 LeftJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func 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 RightJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func 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.