Подтвердить что ты не робот

Метод расширения для IQueryable левого внешнего соединения с использованием LINQ

Я пытаюсь реализовать метод расширения левого внешнего соединения с типом возврата 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<> вместо Expression<>. Я пробовал с помощью Expression<>. Это дает мне ошибку в строке outerKeySelector(outerItem), которая outerKeySelector является переменной, которая используется как метод

Я нашел несколько дискуссий о SO (например, здесь) и CodeProjects, но они работают для IEnumerable не для IQueryable.

4b9b3361

Ответ 1

Введение

Этот вопрос очень интересный. Проблема заключается в том, что Funcs являются делегатами, а выражения - деревья, это совершенно разные структуры. Когда вы используете свою текущую реализацию расширения, она использует циклы и выполняет ваши селекторы на каждом шаге для каждого элемента, и она работает хорошо. Но когда мы говорим о структуре сущности и LINQ, нам нужен обход дерева для перевода его в SQL-запрос. Так что это "немного" сложнее, чем Funcs (но мне все равно нравятся выражения), и есть некоторые проблемы, описанные ниже.

Если вы хотите сделать левое внешнее соединение, вы можете использовать что-то вроде этого (взято здесь: Как реализовать левое соединение в методе расширения 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 ));

Это хорошо, но это не метод расширения, который нам нужен. Думаю, вам нужно что-то вроде этого:

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

В создании таких расширений много сложных частей:

  • Создание сложных деревьев вручную, компилятор нам не поможет.
  • Отражение необходимо для таких методов, как Where, Select и т.д.
  • Анонимные типы (здесь нам нужен codegen, я надеюсь, что нет)

Шаги

Рассмотрим две простые таблицы: A (столбцы: Id, Текст) и B (Идентификаторы столбцов, IdA, Текст).

Внешнее соединение может быть реализовано в три этапа:

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

Надеюсь, это поможет.

Ответ 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 ));
  • Использование Tuple работает, но когда это используется как часть более сложных запросов, EF выходит из строя (не может использовать конструкторы). Чтобы обойти это, вам нужно либо динамически генерировать новый анонимный класс (переполнение стека поиска), либо использовать тип без конструктора. Я создал этот

    internal class KeyValuePairHolder<T1, T2>
    {
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }
    
  • Использование метода Queryable.DefaultIfEmpty. В исходном и в методах GroupJoin правильными методами, выбранными компилятором, являются методы "Enumerable.DefaultIfEmpty". Это не влияет на простой запрос, но заметьте, как принятый ответ имеет кучу конверсий (между IQueryable и IEnumerable). Эти роли также вызывают проблемы в более сложных запросах. Хорошо использовать метод "Enumerable.DefaultIfEmpty" в выражении, EF знает, что он не выполняет его, а вместо этого переводит его в соединение.

  • Наконец, это большая проблема: выполняется два выбора, тогда как оригинал выбирает только один. Вы можете прочитать причину в комментариях кодов (beacuse разницы типов (довольно глубокая проблема: некоторый анонимный тип!= TOuter)) и увидеть его в SQL (выбрать из внутреннего соединения (левое внешнее соединение b)) Проблема здесь в том, что метод Original SelectMany принимает объект, созданный в методе Join типа: KeyValuePairHolder of TOuter и IEnumerable of Tinner в качестве первого параметра, но переданное выражение 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, чтобы избежать использования Tuples, моя обновленная версия выбранного ответа ниже устраняет три проблемы, короче и производит более короткий 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;
        }
    }

Ответ 3

Это метод расширения .LeftJoin, созданный в прошлом году, когда я хотел упростить .GroupJoin. Мне повезло с этим. Я включил комментарии XML, чтобы получить полный intellisense. Там также перегрузка с 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>));
        }

    }
}

Ответ 4

Как указано в предыдущих ответах, когда вы хотите, чтобы ваш IQueryable был переведен на SQL, вам нужно использовать Expression вместо Func, поэтому вам нужно пройти маршрут дерева выражений.

Однако здесь вы можете добиться того же результата, не создавая сами дерево выражений. Фокус в том, что вам нужно ссылаться на LinqKit (доступно через NuGet) и вызывать AsExpandable() в запросе, Это позаботится о построении основного дерева выражений (см. здесь).

В приведенном ниже примере используется метод GroupJoin с SelectMany и DefaultIfEmpty():

код

    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, а переменные users и addresses - это доступ к базовому 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 = адрес .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])

Ответ 5

Обновление моего предыдущего ответа. Когда я разместил его, я не заметил, что вопрос был связан с переводом на SQL. Этот код работает с локальными элементами, поэтому сначала будут вытаскиваться объекты, а затем - вместо внешнего соединения на сервере. Но для обработки нулей с помощью Расширения присоединения, которые я опубликовал ранее, вот пример:

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 = "[email protected]" } },
        new EmailAddress() { Id = 3, Email = new Email() { Name = "George", Address = "[email protected]" } },
        new EmailAddress() { Id = 4, Email = new Email() { Name = "Ringo", Address = "[email protected]" } }
    };

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

}

Ответ 6

@Licentia, это то, что я придумал, чтобы решить вашу проблему. Я создал методы расширения DynamicJoin и DynamicLeftJoin, похожие на то, что вы показали мне, но я обрабатывал результат по-разному, поскольку синтаксический анализ строк уязвим для многих проблем. Это не будет присоединяться к анонимным типам, но вы можете настроить его для этого. Он также не имеет перегрузок для 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 = "[email protected]" } },
        new EmailAddress() { PersonId = 3, Email = new Email() { Name = "George", Address = "[email protected]" } },
        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);
}