Método de extensión para la unión externa izquierda IQueryable utilizando LINQ

Estoy tratando de implementar el método de extensión de unión externa izquierda con el tipo de retorno IQueryable .

La función que he escrito es la siguiente

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

No se puede generar la consulta. La razón podría ser: He usado Func lugar de Expression . Intenté con Expression también. Me da un error en la línea outerKeySelector(outerItem) , que es outerKeySelector es una variable que se utiliza como método

Encontré algunas discusiones sobre SO (como aquí ) y CodeProjects, pero funcionan para tipos de IQueryable no para IQueryable .

Introducción

Esta pregunta es muy interesante. El problema es que las funciones son delegates y las expresiones son árboles , son estructuras completamente diferentes. Cuando utiliza la implementación de su extensión actual, utiliza bucles y ejecuta sus selectores en cada paso para cada elemento y funciona bien. Pero cuando hablamos de enttwigdo de entidades y LINQ, necesitamos un recorrido de árbol para convertirlo en una consulta SQL. Así que es un “pequeño” más difícil que Funcs (pero de todos modos me gustan las Expresiones) y hay algunos problemas que se describen a continuación.

Cuando quiera hacer una combinación externa izquierda, puede usar algo como esto (tomado de aquí: Cómo implementar la combinación izquierda en el método de extensión JOIN )

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

Es bueno, pero no es un método de extensión lo que necesitamos. Supongo que necesitas algo como esto:

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

Hay muchas partes difíciles en la creación de tales extensiones:

  • Creando árboles complejos manualmente, el comstackdor no nos ayudará aquí.
  • La reflexión es necesaria para métodos como Where , Select , etc.
  • Tipos anónimos (¿Necesitamos codegen aquí? Espero que no)

Pasos

Considere 2 tablas simples: A (columnas: Id., Texto) y B (Id. De columnas, IdA, Texto).

La unión externa podría implementarse en 3 pasos:

 // group join as usual + use DefaultIfEmpty var q1 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA, (a, b) => new { a, groupB = b.DefaultIfEmpty() }); // regroup data to associated list a -> b, it is usable already, but it's // impossible to use resultSelector on this stage, // beacuse of type difference (quite deep problem: some anonymous type != TOuter) var q2 = Queryable.SelectMany(q1, x => x.groupB, (a, b) => new { aa, b }); // second regroup to get the right types var q3 = Queryable.SelectMany(db.A, a => q2.Where(x => xa == a).Select(x => xb), (a, b) => new {a, b}); 

Código

Ok, no soy tan buen cajero, aquí está el código que tengo (Lo siento, no pude formatearlo mejor, ¡pero funciona!):

 public static IQueryable LeftOuterJoin2( this IQueryable outer, IQueryable inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector) { // generic methods var selectManies = typeof(Queryable).GetMethods() .Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3) .OrderBy(x=>x.ToString().Length) .ToList(); var selectMany = selectManies.First(); var select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); var where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2); var groupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5); var defaultIfEmpty = typeof(Queryable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1); // need anonymous type here or let's use Tuple // prepares for: // var q2 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA, (a, b) => new { a, groupB = b.DefaultIfEmpty() }); var tuple = typeof(Tuple<,>).MakeGenericType( typeof(TOuter), typeof(IQueryable<>).MakeGenericType( typeof(TInner) ) ); var paramOuter = Expression.Parameter(typeof(TOuter)); var paramInner = Expression.Parameter(typeof(IEnumerable)); var groupJoinExpression = Expression.Call( null, groupJoin.MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), tuple), new Expression[] { Expression.Constant(outer), Expression.Constant(inner), outerKeySelector, innerKeySelector, Expression.Lambda( Expression.New( tuple.GetConstructor(tuple.GetGenericArguments()), new Expression[] { paramOuter, Expression.Call( null, defaultIfEmpty.MakeGenericMethod(typeof (TInner)), new Expression[] { Expression.Convert(paramInner, typeof (IQueryable)) } ) }, tuple.GetProperties() ), new[] {paramOuter, paramInner} ) } ); // prepares for: // var q3 = Queryable.SelectMany(q2, x => x.groupB, (a, b) => new { aa, b }); var tuple2 = typeof (Tuple<,>).MakeGenericType(typeof (TOuter), typeof (TInner)); var paramTuple2 = Expression.Parameter(tuple); var paramInner2 = Expression.Parameter(typeof(TInner)); var paramGroup = Expression.Parameter(tuple); var selectMany1Result = Expression.Call( null, selectMany.MakeGenericMethod(tuple, typeof (TInner), tuple2), new Expression[] { groupJoinExpression, Expression.Lambda( Expression.Convert(Expression.MakeMemberAccess(paramGroup, tuple.GetProperty("Item2")), typeof (IEnumerable)), paramGroup ), Expression.Lambda( Expression.New( tuple2.GetConstructor(tuple2.GetGenericArguments()), new Expression[] { Expression.MakeMemberAccess(paramTuple2, paramTuple2.Type.GetProperty("Item1")), paramInner2 }, tuple2.GetProperties() ), new[] { paramTuple2, paramInner2 } ) } ); // prepares for final step, combine all expressinos together and invoke: // var q4 = Queryable.SelectMany(db.A, a => q3.Where(x => xa == a).Select(x => xb), (a, b) => new { a, b }); var paramTuple3 = Expression.Parameter(tuple2); var paramTuple4 = Expression.Parameter(tuple2); var paramOuter3 = Expression.Parameter(typeof (TOuter)); var selectManyResult2 = selectMany .MakeGenericMethod( typeof(TOuter), typeof(TInner), typeof(TResult) ) .Invoke( null, new object[] { outer, Expression.Lambda( Expression.Convert( Expression.Call( null, select.MakeGenericMethod(tuple2, typeof(TInner)), new Expression[] { Expression.Call( null, where.MakeGenericMethod(tuple2), new Expression[] { selectMany1Result, Expression.Lambda( Expression.Equal( paramOuter3, Expression.MakeMemberAccess(paramTuple4, paramTuple4.Type.GetProperty("Item1")) ), paramTuple4 ) } ), Expression.Lambda( Expression.MakeMemberAccess(paramTuple3, paramTuple3.Type.GetProperty("Item2")), paramTuple3 ) } ), typeof(IEnumerable) ), paramOuter3 ), resultSelector } ); return (IQueryable)selectManyResult2; } 

Uso

Y el uso de nuevo:

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

En cuanto a esto, puedes pensar ¿cuál es la consulta de SQL para todo esto? Podría ser enorme. ¿Adivina qué? Es bastante pequeño:

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

Espero eso ayude.

La respuesta aceptada es un buen comienzo para explicar las complejidades detrás de una unión externa izquierda.

Encontré tres problemas bastante serios con él, especialmente al tomar este método de extensión y usarlo en consultas más complejas (encadenar múltiples uniones externas izquierdas con uniones normales, luego resumir / máximo / contar / …) Antes de copiar la respuesta seleccionada en su entorno de producción, por favor, siga leyendo.

Considere el ejemplo original de la publicación SO vinculada, que representa casi cualquier combinación externa izquierda realizada en LINQ:

 var leftJoin = p.Person.Where(n => n.FirstName.Contains("a")) .GroupJoin(p.PersonInfo, n => n.PersonId, m => m.PersonId, (n, ms) => new { n, ms = ms }) .SelectMany(z => z.ms.DefaultIfEmpty(), (n, m) => new { n = n, m )); 
  • El uso de un Tuple funciona, pero cuando se usa como parte de consultas más complejas, EF falla (no puede usar constructores). Para solucionar esto, necesita generar una nueva clase anónima dinámicamente (desbordamiento de stack de búsqueda) o usar un tipo sin constructor. Creé esto

     internal class KeyValuePairHolder { public T1 Item1 { get; set; } public T2 Item2 { get; set; } } 
  • El uso del método “Queryable.DefaultIfEmpty”. En los métodos original y en los métodos GroupJoin, los métodos correctos que el comstackdor elige son los métodos “Enumerable.DefaultIfEmpty”. Esto no tiene influencia en una consulta simple, pero observe que la respuesta aceptada tiene un montón de conversiones (entre IQueryable y IEnumerable). Los emitidos también causan problemas en consultas más complejas. Está bien usar el método “Enumerable.DefaultIfEmpty” en una expresión, EF sabe que no lo ejecuta, sino que lo traduce en una combinación.

  • Finalmente, este es el problema más grande: hay dos selecciones hechas mientras que el original solo hace una selección. Puede leer la causa en los comentarios del código (debido a la diferencia de tipo (problema bastante profundo: algún tipo anónimo! = Directo)) y verlo en el SQL (seleccionar desde una combinación interna (una combinación externa izquierda b)) El problema aquí es que el método Original SelectMany toma un objeto creado en el método de tipo Join : KeyValuePairHolder de TOuter y IEnumerable de Tinner como su primer parámetro, pero la expresión resultSelector pasada toma un TOUter simple como su primer parámetro. Puede usar un ExpressionVisitor para volver a escribir la expresión que se pasa a la forma correcta.

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

Usando la expresión visitor y KeyValuePairHolder para evitar el uso de Tuples, mi versión actualizada de la respuesta seleccionada a continuación soluciona los tres problemas, es más corta y produce un SQL más corto:

  internal class QueryReflectionMethods { internal static System.Reflection.MethodInfo Enumerable_Select = typeof(Enumerable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); internal static System.Reflection.MethodInfo Enumerable_DefaultIfEmpty = typeof(Enumerable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1); internal static System.Reflection.MethodInfo Queryable_SelectMany = typeof(Queryable).GetMethods().Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3).OrderBy(x => x.ToString().Length).First(); internal static System.Reflection.MethodInfo Queryable_Where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2); internal static System.Reflection.MethodInfo Queryable_GroupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5); internal static System.Reflection.MethodInfo Queryable_Join = typeof(Queryable).GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).First(c => c.Name == "Join"); internal static System.Reflection.MethodInfo Queryable_Select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2); public static IQueryable CreateLeftOuterJoin( IQueryable outer, IQueryable inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector) { var keyValuePairHolderWithGroup = typeof(KeyValuePairHolder<,>).MakeGenericType( typeof(TOuter), typeof(IEnumerable<>).MakeGenericType( typeof(TInner) ) ); var paramOuter = Expression.Parameter(typeof(TOuter)); var paramInner = Expression.Parameter(typeof(IEnumerable)); var groupJoin = Queryable_GroupJoin.MakeGenericMethod(typeof(TOuter), typeof(TInner), typeof(TKey), keyValuePairHolderWithGroup) .Invoke( "ThisArgumentIsIgnoredForStaticMethods", new object[]{ outer, inner, outerKeySelector, innerKeySelector, Expression.Lambda( Expression.MemberInit( Expression.New(keyValuePairHolderWithGroup), Expression.Bind( keyValuePairHolderWithGroup.GetMember("Item1").Single(), paramOuter ), Expression.Bind( keyValuePairHolderWithGroup.GetMember("Item2").Single(), paramInner ) ), paramOuter, paramInner ) } ); var paramGroup = Expression.Parameter(keyValuePairHolderWithGroup); Expression collectionSelector = Expression.Lambda( Expression.Call( null, Enumerable_DefaultIfEmpty.MakeGenericMethod(typeof(TInner)), Expression.MakeMemberAccess(paramGroup, keyValuePairHolderWithGroup.GetProperty("Item2"))) , paramGroup ); Expression newResultSelector = new ResultSelectorRewriter(resultSelector).CombinedExpression; var selectMany1Result = Queryable_SelectMany.MakeGenericMethod(keyValuePairHolderWithGroup, typeof(TInner), typeof(TResult)) .Invoke( "ThisArgumentIsIgnoredForStaticMethods", new object[]{ groupJoin, collectionSelector, newResultSelector } ); return (IQueryable)selectMany1Result; } } 

Como se indicó en las respuestas anteriores, cuando desea que su IQueryable se traduzca a SQL, necesita usar Expression en lugar de Func, por lo que debe ir a la ruta del árbol de expresiones.

Sin embargo, aquí hay una forma en que puede lograr el mismo resultado sin tener que construir el árbol de Expresión usted mismo. El truco es que debe hacer referencia a LinqKit (disponible a través de NuGet) y llamar a AsExpandable () en la consulta. Esto se encargará de construir el árbol de expresión subyacente (vea cómo aquí ).

El siguiente ejemplo utiliza el enfoque GroupJoin con SelectMany y DefaultIfEmpty () :

Código

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

Data de muestra

Supongamos que tenemos las siguientes entidades EF, y las variables de usuarios y direcciones son el acceso al DbSet subyacente:

 public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } public class UserAddress { public int UserId { get; set; } public string LastName { get; set; } public string Street { get; set; } } IQueryable users; IQueryable addresses; 

Uso 1

Unámonos por ID de usuario:

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

Esto se traduce en (usando LinqPad):

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

Uso 2

Ahora unámonos en múltiples propiedades usando un tipo anónimo como clave:

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

Tenga en cuenta que las propiedades de tipo anónimo deben tener los mismos nombres, de lo contrario obtendrá un error de syntax.

Es por eso que tenemos Id = address.UserId en lugar de solo address.UserId .

Esto será traducido a:

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

Este es el método de extensión .LeftJoin que creé el año pasado cuando quise simplificar .GroupJoin. He tenido buena suerte con eso. Incluí los comentarios XML para que obtengas una inteligencia completa. También hay una sobrecarga con un IEqualityComparer. Espero que les sea útil.

Mi paquete completo de Join Extensions está aquí: https://github.com/jolsa/Extensions/blob/master/ExtensionLib/JoinExtensions.cs

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

Una actualización a mi respuesta anterior. Cuando lo publiqué, no me di cuenta de que la pregunta estaba relacionada con la traducción a SQL. Este código funciona en elementos locales, por lo que los objetos se extraerán primero y luego se unirán en lugar de hacer la unión externa en el servidor. Pero para manejar los nulos usando las extensiones de unión que publiqué anteriormente, aquí hay un ejemplo:

 public class Person { public int Id { get; set; } public string Name { get; set; } } public class EmailAddress { public int Id { get; set; } public Email Email { get; set; } } public class Email { public string Name { get; set; } public string Address { get; set; } } public static void Main() { var people = new [] { new Person() { Id = 1, Name = "John" }, new Person() { Id = 2, Name = "Paul" }, new Person() { Id = 3, Name = "George" }, new Person() { Id = 4, Name = "Ringo" } }; var addresses = new[] { new EmailAddress() { Id = 2, Email = new Email() { Name = "Paul", Address = "Paul@beatles.com" } }, new EmailAddress() { Id = 3, Email = new Email() { Name = "George", Address = "George@beatles.com" } }, new EmailAddress() { Id = 4, Email = new Email() { Name = "Ringo", Address = "Ringo@beatles.com" } } }; var joinedById = people.LeftJoin(addresses, p => p.Id, a => a.Id, (p, a) => new { p.Id, p.Name, a?.Email.Address }).ToList(); Console.WriteLine("\r\nJoined by Id:\r\n"); joinedById.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? ""}")); var joinedByName = people.LeftJoin(addresses, p => p.Name, a => a?.Email.Name, (p, a) => new { p.Id, p.Name, a?.Email.Address }, StringComparer.OrdinalIgnoreCase).ToList(); Console.WriteLine("\r\nJoined by Name:\r\n"); joinedByName.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? ""}")); } 

@Licentia, esto es lo que se me ocurrió para resolver su problema. DynamicLeftJoin métodos de extensión DynamicJoin y DynamicLeftJoin similares a los que me mostraste, pero manejé la salida de manera diferente ya que el análisis de cadenas es vulnerable a muchos problemas. Esto no se unirá en tipos anónimos, pero puede modificarlo para hacerlo. Tampoco tiene sobrecargas para IComparable , pero podría agregarse fácilmente. Los nombres de las propiedades deben escribirse igual que el tipo. Esto se usa junto con mis métodos de extensión anteriores (es decir, no funcionará sin ellos). ¡Espero que ayude!

 public class Person { public int Id { get; set; } public string Name { get; set; } } public class EmailAddress { public int PersonId { get; set; } public Email Email { get; set; } } public class Email { public string Name { get; set; } public string Address { get; set; } } public static void Main() { var people = new[] { new Person() { Id = 1, Name = "John" }, new Person() { Id = 2, Name = "Paul" }, new Person() { Id = 3, Name = "George" }, new Person() { Id = 4, Name = "Ringo" } }; var addresses = new[] { new EmailAddress() { PersonId = 2, Email = new Email() { Name = "Paul", Address = "Paul@beatles.com" } }, new EmailAddress() { PersonId = 3, Email = new Email() { Name = "George", Address = "George@beatles.com" } }, new EmailAddress() { PersonId = 4, Email = new Email() { Name = "Ringo" } } }; Console.WriteLine("\r\nInner Join:\r\n"); var innerJoin = people.DynamicJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList(); innerJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? ""}")); Console.WriteLine("\r\nOuter Join:\r\n"); var leftJoin = people.DynamicLeftJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList(); leftJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? ""}")); } public static class DynamicJoinExtensions { private const string OuterPrefix = "outer."; private const string InnerPrefix = "inner."; private class Processor { private readonly Type _typeOuter = typeof(TOuter); private readonly Type _typeInner = typeof(TInner); private readonly PropertyInfo _keyOuter; private readonly PropertyInfo _keyInner; private readonly List _outputFields; private readonly Dictionary _resultProperties; public Processor(string outerKey, string innerKey, IEnumerable outputFields) { _outputFields = outputFields.ToList(); // Check for properties with the same name string badProps = string.Join(", ", _outputFields.Select(f => new { property = f, name = GetName(f) }) .GroupBy(f => f.name, StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) .SelectMany(g => g.OrderBy(f => f.name, StringComparer.OrdinalIgnoreCase).Select(f => f.property))); if (!string.IsNullOrEmpty(badProps)) throw new ArgumentException($"One or more {nameof(outputFields)} are duplicated: {badProps}"); _keyOuter = _typeOuter.GetProperty(outerKey); _keyInner = _typeInner.GetProperty(innerKey); // Check for valid keys if (_keyOuter == null || _keyInner == null) throw new ArgumentException($"One or both of the specified keys is not a valid property"); // Check type compatibility if (_keyOuter.PropertyType != _keyInner.PropertyType) throw new ArgumentException($"Keys must be the same type. ({nameof(outerKey)} type: {_keyOuter.PropertyType.Name}, {nameof(innerKey)} type: {_keyInner.PropertyType.Name})"); Func>> getResultProperties = (prefix, type) => _outputFields.Where(f => f.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) .Select(f => new KeyValuePair(f, type.GetProperty(f.Substring(prefix.Length)))); // Combine inner/outer outputFields with PropertyInfo into a dictionary _resultProperties = getResultProperties(OuterPrefix, _typeOuter).Concat(getResultProperties(InnerPrefix, _typeInner)) .ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase); // Check for properties that aren't found badProps = string.Join(", ", _resultProperties.Where(kv => kv.Value == null).Select(kv => kv.Key)); if (!string.IsNullOrEmpty(badProps)) throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}"); // Check for properties that aren't the right format badProps = string.Join(", ", _outputFields.Where(f => !_resultProperties.ContainsKey(f))); if (!string.IsNullOrEmpty(badProps)) throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}"); } // Inner Join public IEnumerable Join(IEnumerable outer, IEnumerable inner) => outer.Join(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i)); // Left Outer Join public IEnumerable LeftJoin(IEnumerable outer, IEnumerable inner) => outer.LeftJoin(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i)); private static string GetName(string fieldId) => fieldId.Substring(fieldId.IndexOf('.') + 1); private object GetOuterKeyValue(TOuter obj) => _keyOuter.GetValue(obj); private object GetInnerKeyValue(TInner obj) => _keyInner.GetValue(obj); private object GetResultProperyValue(string key, object obj) => _resultProperties[key].GetValue(obj); private dynamic CreateItem(TOuter o, TInner i) { var obj = new ExpandoObject(); var dict = (IDictionary)obj; _outputFields.ForEach(f => { var source = f.StartsWith(OuterPrefix, StringComparison.OrdinalIgnoreCase) ? (object)o : i; dict.Add(GetName(f), source == null ? null : GetResultProperyValue(f, source)); }); return obj; } } public static IEnumerable DynamicJoin(this IEnumerable outer, IEnumerable inner, string outerKey, string innerKey, params string[] outputFields) => new Processor(outerKey, innerKey, outputFields).Join(outer, inner); public static IEnumerable DynamicLeftJoin(this IEnumerable outer, IEnumerable inner, string outerKey, string innerKey, params string[] outputFields) => new Processor(outerKey, innerKey, outputFields).LeftJoin(outer, inner); }