使用LINQ的IQueryable左外连接的扩展方法

我试图实现返回类型为IQueryable Left outer join扩展方法。

我写的function如下

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

它无法生成查询。 原因可能是:我使用了Func而不是Expression 。 我也试过Expression 。 它在outerKeySelector(outerItem)行上给出了一个错误,即outerKeySelector是一个用作方法的变量

我发现了一些关于SO(例如这里 )和CodeProjects的讨论,但是那些适用于IEnumerable类型的讨论不适用于IQueryable

介绍

这个问题非常有趣。 问题是Funcs是委托,而表达式是树 ,它们是完全不同的结构。 当您使用当前的扩展实现时,它使用循环并在每个元素的每个步骤上执行选择器,并且它运行良好。 但是当我们谈论entity framework和LINQ时,我们需要树遍历来将它转换为SQL查询。 所以它比Funcs更“难”(但我还是喜欢Expressions)并且下面描述了一些问题。

当你想做左外连接时你可以使用这样的东西(取自这里: 如何在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 )); 

这是好的,但它不是我们需要的扩展方法。 我想你需要这样的东西:

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

创建此类扩展有许多困难的部分:

  • 手动创建复杂的树,编译器将无法帮助我们
  • WhereSelect等方法需要反思
  • 匿名类型(!!我们需要codegen吗?我希望没有)

脚步

考虑2个简单的表: A (列:Id,文本)和B (列Id,IdA,文本)。

外连接可以分3个步骤实现:

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

好吧,我不是一个好的出纳员,这是我的代码(对不起,我无法更好地格式化,但它有效!):

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

用法

再次使用:

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

看看这个你可以想一下这个sql查询是什么? 这可能是巨大的。 你猜怎么着? 它很小:

 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] 

希望能帮助到你。

接受的答案是解释左外连接背后复杂性的一个很好的开始。

我发现了三个相当严重的问题,特别是在采用这种扩展方法并在更复杂的查询中使用它时(将多个左外连接与正常连接链接,然后汇总/ max / count / …)在将所选答案复制到您的生产环境,请继续阅读。

考虑链接的SOpost中的原始示例,它代表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 )); 
  • 使用Tuple有效,但当这被用作更复杂查询的一部分时,EF失败(不能使用构造函数)。 要解决这个问题,您需要动态生成新的匿名类(搜索堆栈溢出)或使用无构造函数类型。 我创造了这个

     internal class KeyValuePairHolder { public T1 Item1 { get; set; } public T2 Item2 { get; set; } } 
  • 使用“Queryable.DefaultIfEmpty”方法。 在原始方法和GroupJoin方法中,编译器选择的正确方法是“Enumerable.DefaultIfEmpty”方法。 这对简单查询没有影响,但请注意接受的答案如何具有一堆转换(在IQueryable和IEnumerable之间)。 这些演员也会在更复杂的查询中引起问题。 可以在Expression中使用“Enumerable.DefaultIfEmpty”方法,EF知道不执行它,而是将其转换为连接。

  • 最后,这是一个更大的问题:有两个选择完成,而原始只做一个选择。 您可以在代码注释中读取原因(因为类型不同(非常深的问题:一些匿名类型!= TOuter))并在SQL中看到它(从内部联接中选择(左外部联接b))这里的问题原始SelectMany方法采用在Join方法中创建的对象类型: TOuter的KeyValuePairHolder和Tinner的IEnumerable作为它的第一个参数,但传递的resultSelector表达式采用一个简单的TOUter作为它的第一个参数。 您可以使用ExpressionVisitor重写传递给正确forms的表达式。

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

使用表达式visitor和KeyValuePairHolder来避免使用元组,我在下面选择的答案的更新版本修复了三个问题,更短,并产生更短的SQL:

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

如前面的答案中所述,当您希望将IQueryable转换为SQL时,您需要使用Expression而不是Func,因此您必须使用Expression Tree路由。

但是,这里有一种方法可以实现相同的结果,而无需自己构建表达式树。 诀窍是,您需要引用LinqKit (可通过NuGet获得)并在查询上调用AsExpandable() 。 这将负责构建底层表达式树(请参阅此处 )。

下面的示例使用GroupJoinSelectMany以及DefaultIfEmpty()方法:

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

样本数据

假设我们有以下EF实体, 用户地址变量是对底层DbSet的访问:

 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; 

用法1

让我们按用户ID加入:

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

这转换为(使用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] 

用法2

现在让我们使用匿名类型作为键来连接多个属性:

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

请注意,匿名类型属性必须具有相同的名称,否则您将收到语法错误。

这就是为什么我们有Id = address.UserId而不仅仅是address.UserId

这将被翻译为:

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

这是我去年想要简化.GroupJoin时创建的.LeftJoin扩展方法。 我好运。 我包含了XML注释,因此您可以获得完整的智能感知。 IEqualityComparer也有一个重载。 希望对你有帮助。

我的全套Join Extensions就在这里: 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)); } } } 

我之前回答的更新。 当我发布它时,我没有注意到问题在于转换为SQL。 此代码适用于本地项目,因此将首先拉取对象然后连接,而不是在服务器上执行外部联接。 但是要使用我之前发布的Join扩展来处理空值,这是一个例子:

 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,这是我想出来解决你的问题。 我创建了类似于你向我展示的DynamicJoinDynamicLeftJoin扩展方法,但我处理输出的方式不同,因为字符串解析容易受到许多问题的影响。 这不会加入匿名类型,但您可以调整它来执行此操作。 它也没有IComparable重载,但可以很容易地添加。 属性名称必须与类型相同。 这与我上面的扩展方法一起使用(即没有它们就无法工作)。 我希望它有所帮助!

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