sentencias query for example curso c# .net linq outer-join full-outer-join

c# - query - LINQ: unión externa completa



select linq to sql c# (11)

Tengo una lista de la identificación de la persona y su nombre, y una lista de 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 intentos hasta ahora son más o menos así:

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

Pero esto vuelve:

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

¿Qué estoy haciendo mal?


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

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


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


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

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 codificación manual de Expression para manejar la fusión de una Expression<Func<>> en una lambda podría mejorarse con LinqKit, pero sería bueno si el lenguaje / compilador hubiera agregado algo de ayuda para eso. Las funciones FullOuterJoinDistinct y RightOuterJoin se incluyen para completar, pero no reintrodujimos FullOuterGroupJoin todavía.


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<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 { // (lrg,r) => resultSelector(lrg.left, r) var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable<TRight>)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<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 { // (lgr,l) => resultSelector(l, lgr.right) var sampleAnonLR = new { leftg = (IEnumerable<TLeft>)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<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, 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 { // newrightrs = lgr => resultSelector((TLeft)null, lgr.right) 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(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<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)); } public static Expression Apply(this LambdaExpression e, params Expression[] args) { var b = e.Body; foreach (var pa in e.Parameters.Cast<ParameterExpression>().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)); } } }


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


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

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


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 sintaxis y el tipo de retorno del método Enumerable.Join () incorporado que se describe here . Agregué el sufijo "distinto" con respecto al comentario de @ cadrell0 en la solución de @ JeffMercado.

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

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 sintaxis 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 sintaxis 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 genéricos, 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 sintaxis 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 objetivos, después de todo.


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: ideone

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


I like sehe''s answer, but it does not use deferred execution (the input sequences are eagerly enumerated by the calls to ToLookup). So after looking at the .NET sources for LINQ-to-objects , I came up with this:

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

This implementation has the following important properties:

  • Deferred execution, input sequences will not be enumerated before the output sequence is enumerated.
  • Only enumerates the input sequences once each.
  • Preserves order of input sequences, in the sense that it will yield tuples in the order of the left sequence and then the right (for the keys not present in left sequence).

These properties are important, because they are what someone new to FullOuterJoin but experienced with LINQ will expect.


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.


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<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 => m.i.Select(inn => resultSelector(m.o, 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 => m.o.Select(outt => resultSelector(outt, m.i) )); } }