简体   繁体   English

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

[英]Extension method for IQueryable left outer join using LINQ

I am trying to implement Left outer join extension method with return type IQueryable .我正在尝试使用返回类型IQueryable实现左外连接扩展方法。

The function that I have written is as follows我写的函数如下

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

It can't generate the query.它无法生成查询。 The reason might be: I have used Func<> instead of Expression<> .原因可能是:我使用了Func<>而不是Expression<> I tried with Expression<> as well.我也尝试过Expression<> It gives me an error on outerKeySelector(outerItem) line, which is outerKeySelector is a variable which is being used as a method它在outerKeySelector(outerItem)行上给我一个错误,即outerKeySelector是一个用作方法的变量

I found some discussions on SO (such as here ) and CodeProjects, but those work for IEnumerable types not for IQueryable .我发现了一些关于 SO(例如此处)和 CodeProjects 的讨论,但这些讨论适用于IEnumerable类型而不适用于IQueryable

Intro介绍

This question is very interesting.这个问题非常有趣。 The problem is Funcs are delegates and Expressions are trees , they are completely different structures.问题是 Funcs 是委托,而 Expressions 是,它们是完全不同的结构。 When you use your current extension implementation it uses loops and executes your selectors on each step for each element and it works well.当您使用当前的扩展实现时,它使用循环并在每个元素的每个步骤上执行您的选择器,并且运行良好。 But when we talk about entity framework and LINQ we need tree traversal for translation it to SQL query.但是当我们谈论实体框架和 LINQ 时,我们需要树遍历将其转换为 SQL 查询。 So it's a "little" harder than Funcs (but I like Expressions anyway) and there are some problems described below.所以它比 Funcs 难“一点”(但我还是喜欢 Expressions)并且下面描述了一些问题。

When you want to do left outer join you can use something like this (taken from here: How to implement left join in JOIN Extension method )当你想做左外连接时,你可以使用这样的东西(取自这里: 如何在 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 = z.n, m ));

It is good, but it is not extension method we need.这很好,但它不是我们需要的扩展方法。 I guess you need something like this:我想你需要这样的东西:

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

There are many hard parts in creating such extensions:创建这样的扩展有很多困难的部分:

  • Creating complex trees manually, compiler will not help us here手动创建复杂的树,编译器在这里帮不了我们
  • Reflection is needed for methods like Where , Select , etcWhereSelect等方法需要反射
  • Anonymous types (!! we need codegen here?? I hope no)匿名类型(!!我们需要代码生成器?我希望没有)

Steps脚步

Consider 2 simple tables: A (columns: Id, Text) and B (Columns Id, IdA, Text).考虑 2 个简单的表: A (列:Id、Text)和B (列 Id、IdA、Text)。

Outer join could be implemented in 3 steps:外连接可以分 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 { 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});

Code代码

Ok, I'm not such a good teller, here is he code I have (Sorry I was unable to format it better, but it works!):好吧,我不是一个很好的出纳员,这是我的他的代码(对不起,我无法更好地格式化它,但它有效!):

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

Usage用法

And the usage again:再次使用:

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

Looking at this you can think what is the sql query for all this?看这个你可以想这一切的sql查询是什么? It might be huge.它可能是巨大的。 Guess what?你猜怎么着? It's quite small:它很小:

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]

Hope it helps.希望能帮助到你。

The accepted answer is a great start to explain the complexities behind a left outer join.接受的答案是解释左外连接背后复杂性的一个很好的开始。

I found three rather serious issues with it, especially when taking this extension method and using it in more complex queries (chaining multiple left outer joins with normal joins then summarizing/max/count/...) Before you copy the selected answer into your production environment, please do read on.我发现它存在三个相当严重的问题,尤其是在采用这种扩展方法并在更复杂的查询中使用它时(将多个左外连接与普通连接链接起来,然后汇总/最大/计数/...)生产环境,请继续阅读。

Consider the original example from the linked SO post, which represents just about any left outer join done in LINQ:考虑链接的 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 ));
  • The usage of a Tuple works, but when this is used as part of more complex queries, EF fails (cannot use constructors).元组的用法有效,但是当它用作更复杂查询的一部分时,EF 失败(不能使用构造函数)。 To get around this, you either need to generate a new anonymous class dynamically (search stack overflow) or use a constructor-less type.要解决此问题,您需要动态生成新的匿名类(搜索堆栈溢出)或使用无构造函数类型。 I created this我创造了这个

    internal class KeyValuePairHolder<T1, T2> { public T1 Item1 { get; set; } public T2 Item2 { get; set; } }
  • The usage of the "Queryable.DefaultIfEmpty" method. “Queryable.DefaultIfEmpty”方法的用法。 In the original and in the GroupJoin methods, correct methods that are chosen by the compiler are the "Enumerable.DefaultIfEmpty" methods.在原始方法和 GroupJoin 方法中,编译器选择的正确方法是“Enumerable.DefaultIfEmpty”方法。 This has no influence in a simple query, but notice how the accepted answer has a bunch of Converts (between IQueryable and IEnumerable).这对简单查询没有影响,但请注意接受的答案如何有一堆转换(在 IQueryable 和 IEnumerable 之间)。 Those cast also cause issues in more complex queries.这些转换还会导致更复杂的查询出现问题。 It's ok to use the "Enumerable.DefaultIfEmpty" method in an Expression, EF knows not to execute it but to translate it into a join instead.可以在表达式中使用“Enumerable.DefaultIfEmpty”方法,EF 知道不执行它而是将其转换为连接。

  • Finally, this is the bigger issue: there are two selects done whereas the original only does one select.最后,这是一个更大的问题:有两个选择完成,而原始只有一个选择。 You can read the cause in the code comments (beacuse of type difference (quite deep problem: some anonymous type != TOuter)) and see it in the SQL (Select from A inner join (a left outer join b)) The issue here is that the Original SelectMany method takes an object created in the Join method of type: KeyValuePairHolder of TOuter and IEnumerable of Tinner as it's first parameter, but the resultSelector expression passed takes a simple TOUter as it's first parameter.您可以在代码注释中阅读原因(因为类型差异(相当深的问题:一些匿名类型!= TOuter))并在 SQL 中看到它(从 A 内连接中选择(a 左外连接 b))这里的问题是原始 SelectMany 方法采用在 Join 方法中创建的类型为 TOuter 的 KeyValuePairHolder 和 Tinner 的 IEnumerable 的对象作为它的第一个参数,但传递的 resultSelector 表达式采用一个简单的 TOUTer 作为它的第一个参数。 You can use an ExpressionVisitor to rewrite the expression that is passed into the correct form.您可以使用 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)); } }

Using the expression visitor and KeyValuePairHolder to avoid usage of Tuples, my updated version of the selected answer below fixes the three issues, is shorter, and produces shorter SQL:使用表达式访问者和 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;
        }
    }

As stated in previous answers, when you want your IQueryable to be translated into SQL you need to use Expression instead of Func, so you have to go the Expression Tree route.如之前的答案所述,当您希望将 IQueryable 转换为 SQL 时,您需要使用 Expression 而不是 Func,因此您必须走 Expression Tree 路线。

However, here's a way you can achieve the same result without having to build the Expression tree yourself.但是,这里提供了一种无需自己构建表达式树即可获得相同结果的方法。 The trick is, you need to reference LinqKit (available via NuGet) and call AsExpandable() on the query.诀窍是,您需要引用LinqKit (可通过 NuGet 获得)并在查询上调用AsExpandable() This will take care of building the underlying expression tree (see how here ).这将负责构建底层表达式树(请参阅此处的操作方法)。

The example below uses the GroupJoin with SelectMany and DefaultIfEmpty() approach:下面的示例使用GroupJoin with SelectManyDefaultIfEmpty()方法:

Code代码

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

Sample Data样本数据

Let's assume we have the following EF entities, and the users and addresses variables are the access to the underlying DbSet:假设我们有以下 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<User> users;
IQueryable<UserAddress> addresses;

Usage 1用法 1

Let's join by user id:让我们通过用户 id 加入:

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

This translates to (using LinqPad):这转化为(使用 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]

Usage 2用法2

Now let's join on multiple properties using an anonymous type as key:现在让我们使用匿名类型作为键加入多个属性:

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

Please note that the anonymous type properties must have the same names, otherwise you'll get a syntax error.请注意匿名类型属性必须具有相同的名称,否则会出现语法错误。

That's why we have Id = address.UserId instead of just address.UserId .这就是为什么我们有Id = address.UserId而不仅仅是address.UserId

This will be translated to:这将被翻译成:

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

This is the .LeftJoin extension method I created last year when I wanted to simplify the .GroupJoin.这是我去年想简化 .GroupJoin 时创建的 .LeftJoin 扩展方法。 I've had good luck with it.我很幸运。 I included the XML comments so you get full intellisense.我包含了 XML 注释,以便您获得完整的智能感知。 There's also an overload with an IEqualityComparer.还有一个 IEqualityComparer 的重载。 I hope you find it useful.希望对你有帮助。

My full suite of Join Extensions is here: https://github.com/jolsa/Extensions/blob/master/ExtensionLib/JoinExtensions.cs我的全套加入扩展在这里: 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>));
        }

    }
}

An update to my previous answer.更新我之前的回答。 When I posted it, I didn't notice that the question was around translating to SQL.当我发布它时,我没有注意到问题是关于翻译成 SQL。 This code works on local items, so the objects will be pulled first and then joined instead of doing the outer join on the server.此代码适用于本地项目,因此将首先拉取对象然后加入,而不是在服务器上进行外部联接。 But to handle nulls using the Join extensions I posted earlier, here's an example:但是为了使用我之前发布的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 ?? "<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>"}"));

}

@Licentia, this is what I came up with to resolve your issue. @Licentia,这就是我想出的办法来解决您的问题。 I created DynamicJoin and DynamicLeftJoin extension methods similar to what you showed me, but I handled the output differently since string parsing is vulnerable to many problems.我创建了DynamicJoinDynamicLeftJoin扩展方法,类似于您向我展示的方法,但我以不同的方式处理输出,因为字符串解析容易受到许多问题的影响。 This won't join on anonymous types, but you can tweak it to do so.这不会加入匿名类型,但您可以调整它来这样做。 It also doesn't have overloads for IComparable , but could easily be added.它也没有IComparable重载,但可以轻松添加。 Property names must be cased the same as the type.属性名称的大小写必须与类型相同。 This is used in conjunction with my extension methods above (ie it won't work without them).这与我上面的扩展方法结合使用(即没有它们就无法工作)。 I hope it helps!我希望它有帮助!

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM