LINQ: unión externa completa

Tengo una lista de la identificación de la persona y su nombre, y una lista de la identificación de la persona y su apellido. Algunas personas no tienen un nombre y otras no tienen un apellido; Me gustaría hacer una combinación externa completa en las dos listas.

Entonces las siguientes listas:

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

Debe producir:

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

Soy nuevo en LINQ (así que discúlpeme si estoy siendo cojo) y he encontrado bastantes soluciones para ‘LINQ Outer Joins’ que se parecen bastante, pero que realmente parecen ser uniones externas.

Mis bashs hasta ahora son más o menos así:

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

Pero esto vuelve:

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

¿Qué estoy haciendo mal?

No sé si esto cubre todos los casos, lógicamente parece correcto. La idea es tomar una combinación externa izquierda y una combinación externa derecha y combinarlas juntas (como debería ser).

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

Esto funciona como está escrito ya que está en LINQ to Objects. Si LINQ to SQL u otro, la sobrecarga de DefaultIfEmpty() que toma de forma predeterminada puede no funcionar. Entonces tendría que usar el operador condicional para obtener los valores de manera condicional.

es decir,

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

Actualización 1: proporcionar un método de extensión verdaderamente generalizado FullOuterJoin
Actualización 2: opcionalmente acepta un IEqualityComparer personalizado para el tipo de clave
Actualización 3 : esta implementación se ha convertido recientemente en parte de MoreLinq – ¡Gracias chicos!

Editar agregado FullOuterGroupJoin ( ideone ). GetOuter<> implementación de GetOuter<> , lo que hace que esta fracción sea menos GetOuter<> de lo que podría ser, pero estoy buscando un código de “alto nivel”, no optimizado, en este momento.

Véalo en vivo en 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); } 

Imprime el resultado:

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

También puede proporcionar los valores predeterminados: 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)" } ) 

Impresión:

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

Explicación de los términos usados:

Unirse es un término tomado del diseño de una base de datos relacional:

  • Una unión repetirá elementos de a tantas veces como haya elementos en b con la tecla correspondiente (es decir: nada si b estuviera vacío). La jerga de la base de datos llama a esto inner (equi)join .
  • Una combinación externa incluye elementos de a para los cuales no existe un elemento correspondiente en b . (es decir: incluso los resultados si b estaban vacíos). Esto generalmente se conoce como left join .
  • Una combinación externa completa incluye registros de a y b si no existe ningún elemento correspondiente en la otra. (es decir, incluso los resultados si a estaban vacíos)

Algo que normalmente no se ve en RDBMS es un grupo unirse [1] :

  • Un grupo unirse , hace lo mismo que se describió anteriormente, pero en lugar de repetir elementos de a para múltiples b correspondientes, agrupa los registros con las claves correspondientes. Esto a menudo es más conveniente cuando desea enumerar a través de registros ‘unidos’, basados ​​en una clave común.

Consulte también GroupJoin, que también contiene algunas explicaciones generales de antecedentes.


[1] (Creo que Oracle y MSSQL tienen extensiones propietarias para esto)

Código completo

Una clase de extensión ‘drop-in’ generalizada para este

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

Creo que hay problemas con la mayoría de estos, incluida la respuesta aceptada, porque no funcionan bien con Linq sobre IQueryable debido a que hace demasiados viajes de ida y vuelta al servidor y demasiadas devoluciones de datos, o hace demasiada ejecución del cliente.

Para IEnumerable no me gusta la respuesta de Sehe o similar porque tiene un uso excesivo de memoria (una simple prueba de dos listas 10000000 ejecutó Linqpad sin memoria en mi máquina de 32GB).

Además, la mayoría de los otros no implementan realmente una unión externa completa adecuada porque están utilizando una Unión con una unión correcta en lugar de Concat con una unión a la derecha, lo que no solo elimina las filas de unión interna duplicadas del resultado, sino que cualquier duplicado correcto que existió originalmente en los datos de la izquierda o la derecha.

Así que aquí están mis extensiones que manejan todos estos problemas, generan SQL y implementan la unión directamente en Linq, ejecutándose en el servidor, y es más rápido y con menos memoria que otros en 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 diferencia entre un Anti-Semi-Join correcto es mayormente discutible con Linq to Objects o en la fuente, pero hace una diferencia en el lado del servidor (SQL) en la respuesta final, eliminando un JOIN innecesario.

La encoding manual de Expression para manejar la fusión de una Expression> en una lambda podría mejorarse con LinqKit, pero sería bueno si el lenguaje / comstackdor hubiera agregado algo de ayuda para eso. Las funciones FullOuterJoinDistinct y RightOuterJoin se incluyen para completar, pero no reintrodujimos FullOuterGroupJoin todavía.

Escribí otra versión de una combinación externa completa para IEnumerable para los casos en que la clave es ordenable, que es aproximadamente un 50% más rápida que combinar la combinación externa izquierda con la anti semi unión correcta, al menos en colecciones pequeñas. Revisa cada colección después de ordenarla solo una vez.

Aquí hay un método de extensión que hace eso:

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

Como has encontrado, Linq no tiene una construcción de “unión externa”. Lo más cercano que puede obtener es una combinación externa izquierda usando la consulta que indicó. A esto, puede agregar cualquier elemento de la lista de apellidos que no esté representado en la unión:

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

Supongo que el enfoque de @ sehe es más fuerte, pero hasta que lo entiendo mejor, me encuentro saltando de la extensión de @ MichaelSander. Lo modifiqué para que coincida con la syntax y el tipo de retorno del método Enumerable.Join () incorporado que se describe aquí . Agregué el sufijo “distinto” con respecto al comentario de @ cadrell0 en la solución 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); } } 

En el ejemplo, lo usarías así:

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

En el futuro, a medida que sepa más, tengo la sensación de que migraré a la lógica de @ sehe dada su popularidad. Pero incluso así tendré que tener cuidado, porque creo que es importante tener al menos una sobrecarga que coincida con la syntax del método existente “.Join ()” si es factible, por dos razones:

  1. La consistencia en los métodos ayuda a ahorrar tiempo, evitar errores y evitar el comportamiento involuntario.
  2. Si alguna vez hay un método listo para usar “.FullJoin ()” en el futuro, me imagino que intentará mantener la syntax del método “.Join ()” actualmente existente si puede. Si lo hace, entonces si desea migrar a él, simplemente puede cambiar el nombre de sus funciones sin cambiar los parámetros o preocuparse por los diferentes tipos de devolución que rompen su código.

Todavía soy nuevo con generics, extensiones, declaraciones de Func y otras características, por lo que los comentarios son bienvenidos.

EDITAR: No tardé en darme cuenta de que había un problema con mi código. Estaba haciendo un .Dump () en LINQPad y mirando el tipo de devolución. Era solo IEnumerable, así que traté de combinarlo. Pero cuando hice un .Where () o .Seleccione () en mi extensión, recibí un error: “‘System Collections.IEnumerable’ no contiene una definición para ‘Seleccionar’ y …”. Así que al final pude igualar la syntax de entrada de .Join (), pero no el comportamiento de retorno.

EDITAR: Agregó “TResult” al tipo de retorno para la función. Se perdió cuando se lee el artículo de Microsoft, y por supuesto tiene sentido. Con esta solución, ahora parece que el comportamiento de retorno está en línea con mis objectives, después de todo.

Me gusta la respuesta de Sehe, pero no utiliza la ejecución diferida (las secuencias de entrada se enumeran ansiosamente por las llamadas a ToLookup). Entonces, después de mirar las fonts .NET para LINQ-to-objects , se me ocurrió esto:

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

Esta implementación tiene las siguientes propiedades importantes:

  • Ejecución diferida, las secuencias de entrada no se enumerarán antes de enumerar la secuencia de salida.
  • Solo enumera las secuencias de entrada una vez cada una.
  • Conserva el orden de las secuencias de entrada, en el sentido de que producirá tuplas en el orden de la secuencia izquierda y luego la derecha (para las claves que no están en la secuencia izquierda).

Estas propiedades son importantes, porque son lo que alguien nuevo en FullOuterJoin experimentará con LINQ.

Realiza una enumeración de transmisión en memoria en memoria sobre ambas entradas e invoca el selector para cada fila. Si no hay una correlación en la iteración actual, uno de los argumentos del selector será nulo .

Ejemplo:

  var result = left.FullOuterJoin( right, x=>left.Key, x=>right.Key, (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key }); 
  • Requiere un IComparer para el tipo de correlación, usa el Comparer.Default si no se proporciona.

  • Requiere que ‘OrderBy’ se aplique a los enumerables de entrada

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

Decidí agregar esto como una respuesta por separado, ya que no estoy seguro de que esté suficientemente probado. Esta es una reimplementación del método FullOuterJoin que utiliza esencialmente una versión simplificada y personalizada de LINQKit Invoke / Expand for Expression para que funcione el Entity Framework. No hay mucha explicación, ya que es más o menos lo mismo que mi respuesta anterior.

 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. Espero eso ayude.

 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.