【问题标题】:Extension method for IQueryable left outer join using LINQ使用 LINQ 的 IQueryable 左外连接的扩展方法
【发布时间】:2014-03-04 03:40:42
【问题描述】:

我正在尝试实现返回类型为IQueryable 的左外连接扩展方法。

我写的函数如下

public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<TOuter, TInner, TResult> 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&lt;&gt; 而不是Expression&lt;&gt;。我也试过Expression&lt;&gt;。它在outerKeySelector(outerItem) 行上给我一个错误,即outerKeySelector 是一个被用作方法的变量

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

【问题讨论】:

  • 您收到的确切错误消息是什么?我的想法是IQueryable 实际上 一个IEnumerable,因此适用于 IEnumerable 的方法也应该适用于这个实例,你是否尝试过使用适用于IEnumerable 的方法和然后通过调用.AsQueryable() 简单地转换为IQueryable
  • 不同之处在于,当 IEnumerable 是 LINQ to Objects 的基础时,查询提供程序将 IQueryable 转换为正确的 SQL,然后针对数据库执行该 SQL。 IQueryable 需要表达式树作为参数,IEnumerable 可以用于委托。

标签: c# .net linq


【解决方案1】:

简介

这个问题很有趣。问题是 Funcs 是代表,而 Expressions 是trees,它们是完全不同的结构。当您使用当前的扩展实现时,它会使用循环并在每个步骤的每个元素上执行您的选择器,并且效果很好。但是当我们谈论实体框架和 LINQ 时,我们需要树遍历来将其转换为 SQL 查询。所以它比 Funcs 难“一点”(但我还是喜欢 Expressions),并且有一些问题如下所述。

当你想做左外连接时,你可以使用这样的东西(取自这里:How to implement left join in JOIN Extension method

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 = z.n, 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 等方法需要反射
  • 匿名类型(!!我们这里需要代码生成??我希望没有)

步骤

考虑 2 个简单的表:A(列:Id、Text)和 B(列 Id、IdA、Text)。

外连接可以分三步实现:

// 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 { a.a, b });

// second regroup to get the right types
var q3 = Queryable.SelectMany(db.A, 
                               a => q2.Where(x => x.a == a).Select(x => x.b), 
                               (a, b) => new {a, b});

代码

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

public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> 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<TInner>));
        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<TInner>))
                                            }
                                )
                                },
                            tuple.GetProperties()
                            ),
                        new[] {paramOuter, paramInner}
                )
                }
            );

        // prepares for:
        // var q3 = Queryable.SelectMany(q2, x => x.groupB, (a, b) => new { a.a, 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<TInner>)),
                        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 => x.a == a).Select(x => x.b), (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<TInner>)
                            ),
                            paramOuter3
                        ),
                        resultSelector
                    }
            );

        return (IQueryable<TResult>)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]

希望对您有所帮助。

【讨论】:

  • 这基本上也是 LINQ“语言”的全部原因——一旦你进入连接,只使用扩展方法做任何事情都是一个巨大的痛苦。 LINQ 关键字方式生成更易于阅读的代码(即使它在幕后做同样的事情)。
  • 一段时间以来看到的最英勇的回答。
【解决方案2】:

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

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

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

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

  • 最后,这是一个更大的问题:有两个选择完成,而原来的只有一个选择。您可以在代码 cmets (beacuse of type difference (相当深的问题: some anonymous type != TOuter))中阅读原因并在 SQL 中看到它(Select from A inner join (左外连接 b)) 这里的问题是,原始 SelectMany 方法采用在 Join 方法中创建的对象类型:TOuter 的 KeyValuePairHolder 和 Tinner 的 IEnumerable 作为它的第一个参数,但是传递的 resultSelector 表达式采用简单的 TOUTer第一个参数。您可以使用 ExpressionVisitor 将传递的表达式重写为正确的形式。

    internal class ResultSelectorRewriter<TOuter, TInner, TResult> : ExpressionVisitor
    {
        private Expression<Func<TOuter, TInner, TResult>> resultSelector;
        public Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>> CombinedExpression { get; private set; }
    
        private ParameterExpression OldTOuterParamExpression;
        private ParameterExpression OldTInnerParamExpression;
        private ParameterExpression NewTOuterParamExpression;
        private ParameterExpression NewTInnerParamExpression;
    
    
        public ResultSelectorRewriter(Expression<Func<TOuter, TInner, TResult>> resultSelector)
        {
            this.resultSelector = resultSelector;
            this.OldTOuterParamExpression = resultSelector.Parameters[0];
            this.OldTInnerParamExpression = resultSelector.Parameters[1];
    
            this.NewTOuterParamExpression = Expression.Parameter(typeof(KeyValuePairHolder<TOuter, IEnumerable<TInner>>));
            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<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, 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));
    
        } 
    }
    

使用表达式访问者和 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<TResult> CreateLeftOuterJoin<TOuter, TInner, TKey, TResult>(
                   IQueryable<TOuter> outer,
                   IQueryable<TInner> inner,
                   Expression<Func<TOuter, TKey>> outerKeySelector,
                   Expression<Func<TInner, TKey>> innerKeySelector,
                   Expression<Func<TOuter, TInner, TResult>> 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<TInner>));
            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<TOuter, TInner, TResult>(resultSelector).CombinedExpression;


            var selectMany1Result =
                Queryable_SelectMany.MakeGenericMethod(keyValuePairHolderWithGroup, typeof(TInner), typeof(TResult))
                .Invoke(
                    "ThisArgumentIsIgnoredForStaticMethods", new object[]{
                        groupJoin,
                        collectionSelector,
                        newResultSelector
                    }
                );
            return (IQueryable<TResult>)selectMany1Result;
        }
    }

【讨论】:

  • 虽然您的方法似乎适用于 EF6,但我已尝试将您建议的方法与 EF Core 2.0 一起使用,但未能获得结果。我不确定这是否是 EF Core 2.0 的错误。我在这里问过这个问题:stackoverflow.com/questions/46537158/…
【解决方案3】:

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

但是,您可以通过以下方式获得相同的结果,而无需自己构建表达式树。诀窍是,您需要引用 LinqKit(可通过 NuGet 获得)并在查询中调用 AsExpandable()。这将负责构建底层表达式树(查看here 的方法)。

下面的示例使用 GroupJoinSelectManyDefaultIfEmpty() 方法:

代码

    public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> 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 实体,usersaddresses 变量是对底层 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<User> users;
IQueryable<UserAddress> 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])

【讨论】:

    【解决方案4】:

    这是我去年创建的 .LeftJoin 扩展方法,当时我想简化 .GroupJoin。我很幸运。我包含了 XML cmets,因此您可以获得完整的智能感知。还有一个 IEqualityComparer 的重载。我希望你觉得它有用。

    我的全套加入扩展在这里:https://github.com/jolsa/Extensions/blob/master/ExtensionLib/JoinExtensions.cs

    // JoinExtensions: Created 07/12/2014 - Johnny Olsa
    
    using System.Linq;
    
    namespace System.Collections.Generic
    {
        /// <summary>
        /// Join Extensions that .NET should have provided?
        /// </summary>
        public static class JoinExtensions
        {
            /// <summary>
            /// Correlates the elements of two sequences based on matching keys. A specified
            /// System.Collections.Generic.IEqualityComparer&lt;T&gt; is used to compare keys.
            /// </summary>
            /// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam>
            /// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam>
            /// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam>
            /// <typeparam name="TResult">The type of the result elements.</typeparam>
            /// <param name="outer">The first sequence to join.</param>
            /// <param name="inner">The sequence to join to the first sequence.</param>
            /// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param>
            /// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
            /// <param name="resultSelector">A function to create a result element from two combined elements.</param>
            /// <param name="comparer">A System.Collections.Generic.IEqualityComparer&lt;T&gt; to hash and compare keys.</param>
            /// <returns>
            /// An System.Collections.Generic.IEnumerable&lt;T&gt; that has elements of type TResult
            /// that are obtained by performing an left outer join on two sequences.
            /// </returns>
            /// <example>
            /// Example:
            /// <code>
            /// 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();
            ///        }
            ///    }
            ///    </code>
            /// </example>
            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, IEqualityComparer<TKey> comparer)
            {
                return outer.GroupJoin(
                    inner,
                    outerKeySelector,
                    innerKeySelector,
                    (o, ei) => ei
                        .Select(i => resultSelector(o, i))
                        .DefaultIfEmpty(resultSelector(o, default(TInner))), comparer)
                        .SelectMany(oi => oi);
            }
    
            /// <summary>
            /// Correlates the elements of two sequences based on matching keys. The default
            /// equality comparer is used to compare keys.
            /// </summary>
            /// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam>
            /// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam>
            /// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam>
            /// <typeparam name="TResult">The type of the result elements.</typeparam>
            /// <param name="outer">The first sequence to join.</param>
            /// <param name="inner">The sequence to join to the first sequence.</param>
            /// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param>
            /// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
            /// <param name="resultSelector">A function to create a result element from two combined elements.</param>
            /// <returns>
            /// An System.Collections.Generic.IEnumerable&lt;T&gt; that has elements of type TResult
            /// that are obtained by performing an left outer join on two sequences.
            /// </returns>
            /// <example>
            /// Example:
            /// <code>
            /// 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();
            ///        }
            ///    }
            ///    </code>
            /// </example>
            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.LeftJoin(inner, outerKeySelector, innerKeySelector, resultSelector, default(IEqualityComparer<TKey>));
            }
    
        }
    }
    

    【讨论】:

    • 我刚刚意识到我的扩展是针对 IEnumerable 而不是 IQueryable。当我在 LINQPad 中测试我的 .LeftJoin 并查看 SQL 时,它会提取两个表并在本地执行 .LeftJoin,因此需要考虑这一点。出于我的目的,我总是使用本地数据,所以没关系。我只是想明确一点,带有这些扩展的服务器上不会发生 LEFT JOIN。
    • ...我意识到我没有彻底阅读最初专门寻找 IQueryable 的问题,所以很抱歉。只要您不需要在服务器上发生左连接,这些扩展方法仍然有用。
    • 我正在使用您的 LeftJoin 扩展方法,但是当我将它们链接在一起时出现空引用异常。
    • 是否可以修改代码以接受父键、子键和选择器的字符串?
    • @Justin,你能给我举个例子吗? LeftJoin 存在固有的 null 问题,我将添加一条新注释,并提供几种处理方法。当我发布这个时,我没有注意到问题是关于翻译成 SQL 的。此代码适用于本地项目,因此将首先拉出对象并然后加入,而不是在服务器上进行外部加入。
    【解决方案5】:

    更新我之前的答案。当我发布它时,我没有注意到问题是关于翻译成 SQL 的。此代码适用于本地项目,因此将首先拉出对象并然后加入,而不是在服务器上进行外部加入。但是要使用我之前发布的 Join extensions 处理空值,这里有一个示例:

    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 ?? "<null>"}"));
    
        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 ?? "<null>"}"));
    
    }
    

    【讨论】:

    • @RaduV 为处理服务器连接提供了出色的解决方案。我试过了,我喜欢它。我会补充一点,如果适用,我更喜欢IEnumerable&lt;T&gt; 连接,因为您不限于与数据库兼容的语法。但在服务器上执行内部/外部联接以提高性能并限制要处理的数据量是有益的。
    【解决方案6】:

    @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 ?? "<null>"}"));
    
        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 ?? "<null>"}"));
    
    }
    
    public static class DynamicJoinExtensions
    {
        private const string OuterPrefix = "outer.";
        private const string InnerPrefix = "inner.";
    
        private class Processor<TOuter, TInner>
        {
            private readonly Type _typeOuter = typeof(TOuter);
            private readonly Type _typeInner = typeof(TInner);
            private readonly PropertyInfo _keyOuter;
            private readonly PropertyInfo _keyInner;
            private readonly List<string> _outputFields;
            private readonly Dictionary<string, PropertyInfo> _resultProperties;
    
            public Processor(string outerKey, string innerKey, IEnumerable<string> 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<string, Type, IEnumerable<KeyValuePair<string, PropertyInfo>>> getResultProperties = (prefix, type) =>
                   _outputFields.Where(f => f.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                       .Select(f => new KeyValuePair<string, PropertyInfo>(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<dynamic> Join(IEnumerable<TOuter> outer, IEnumerable<TInner> inner) =>
                outer.Join(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i));
            //  Left Outer Join
            public IEnumerable<dynamic> LeftJoin(IEnumerable<TOuter> outer, IEnumerable<TInner> 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<string, object>)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<dynamic> DynamicJoin<TOuter, TInner>(this IEnumerable<TOuter> outer,
                IEnumerable<TInner> inner, string outerKey, string innerKey,
                params string[] outputFields) =>
            new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).Join(outer, inner);
        public static IEnumerable<dynamic> DynamicLeftJoin<TOuter, TInner>(this IEnumerable<TOuter> outer,
                IEnumerable<TInner> inner, string outerKey, string innerKey,
                params string[] outputFields) =>
            new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).LeftJoin(outer, inner);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-09
      • 1970-01-01
      • 2013-05-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多