web-dev-qa-db-fra.com

Méthode d'extension pour la jointure externe gauche IQueryable à l'aide de LINQ

J'essaie d'implémenter la méthode d'extension de jointure externe gauche avec le type de retour IQueryable

La fonction que j'ai écrite est la suivante

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

Il ne peut pas générer la requête. La raison pourrait être: j'ai utilisé Func<> au lieu de Expression<>. J'ai aussi essayé avec Expression<>. Cela me donne une erreur sur la ligne outerKeySelector(outerItem), qui est outerKeySelector est une variable qui est utilisée comme méthode

J'ai trouvé des discussions sur SO (comme ici ) et CodeProjects, mais celles-ci fonctionnent pour les types IEnumerable et non pour IQueryable.

19
N Rocking

Intro

Cette question est très intéressante. Le problème est que les fonctions sont des délégués et que les expressions sont des arbres , ce sont des structures complètement différentes. Lorsque vous utilisez votre implémentation d'extension actuelle, il utilise des boucles et exécute vos sélecteurs à chaque étape pour chaque élément et cela fonctionne bien. Mais lorsque nous parlons de structure d'entité et de LINQ, nous avons besoin d'une arborescence pour la traduction en requête SQL. C'est donc un "peu" plus difficile que Funcs (mais j'aime bien les expressions) et quelques problèmes sont décrits ci-dessous.

Lorsque vous voulez faire une jointure externe gauche, vous pouvez utiliser quelque chose comme ceci (pris à partir d'ici: Comment implémenter une jointure gauche dans la méthode JOIN Extension )

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

C'est bien, mais ce n'est pas une méthode d'extension dont nous avons besoin. Je suppose que vous avez besoin de quelque chose comme ça:

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

La création de telles extensions comporte de nombreuses difficultés:

  • Créer des arbres complexes manuellement, le compilateur ne nous aidera pas ici
  • Une réflexion est nécessaire pour des méthodes telles que Where, Select, etc.
  • Types anonymes (!! nous avons besoin de code ici? J'espère que non)

Pas

Considérez 2 tableaux simples: A (colonnes: Id, Texte) et B (Colonnes Id, IdA, Texte).

La jointure externe pourrait être mise en œuvre en 3 étapes:

// 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, je ne suis pas un bon conteur, voici le code que j'ai (désolé, je ne pouvais pas mieux le formater, mais ça marche!):

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

Utilisation

Et encore l'usage:

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

En regardant cela, vous pouvez penser quelle est la requête SQL pour tout cela? Cela pourrait être énorme. Devine quoi? C'est assez petit:

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]

J'espère que ça aide. 

27
Tony

La réponse acceptée est un bon début pour expliquer la complexité d’une jointure externe gauche.

J'y ai trouvé trois problèmes assez graves, en particulier lorsque cette méthode d'extension est utilisée et utilisée dans des requêtes plus complexes (chaînage de plusieurs jointures externes gauches avec des jointures normales, puis synthèse/max/count /...) Avant de copier la sélection répondre dans votre environnement de production, veuillez lire la suite.

Prenons l'exemple original de la publication SO liée, qui représente à peu près n'importe quelle jointure externe gauche réalisée dans 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 ));
  • L'utilisation d'un tuple fonctionne, mais EF le fait échouer (il ne peut pas utiliser de constructeur) lorsqu'il est utilisé dans le cadre de requêtes plus complexes. Pour contourner ce problème, vous devez soit générer une nouvelle classe anonyme (dépassement de capacité de la pile de recherche), soit utiliser un type sans constructeur. J'ai créé ce 

    internal class KeyValuePairHolder<T1, T2>
    {
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }
    
  • Utilisation de la méthode "Queryable.DefaultIfEmpty". Dans les méthodes d'origine et GroupJoin, les méthodes correctes choisies par le compilateur sont les méthodes "Enumerable.DefaultIfEmpty". Cela n’a aucune influence dans une requête simple, mais remarquez que la réponse acceptée a un tas de conversions (entre IQueryable et IEnumerable). Celles-ci causent également des problèmes dans les requêtes plus complexes. Il est normal d'utiliser la méthode "Enumerable.DefaultIfEmpty" dans une expression, EF ne sait pas l'exécuter mais le traduire en jointure à la place.

  • Enfin, c’est le problème le plus important: deux sélections sont effectuées, alors que l’original ne l’est que. Vous pouvez lire la cause dans les commentaires de code (beacuse de différence de type (problème assez profond: certains types anonymes! = TOuter)) et le voir dans le SQL (Sélectionner dans Une jointure interne (une jointure externe gauche b)) Le problème ici est que la méthode SelectMany d'origine prend un objet créé dans la méthode Join de type: KeyValuePairHolder de TOuter et IEnumerable de Tinner en tant que premier paramètre, mais l'expression resultSelector transmise prend un simple TOUter comme premier paramètre. Vous pouvez utiliser un ExpressionVisitor pour réécrire l'expression transmise dans le bon formulaire.

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

En utilisant l'expression visiteur et KeyValuePairHolder pour éviter l'utilisation de Tuples, la version mise à jour de la réponse sélectionnée ci-dessous corrige les trois problèmes, est plus courte et produit un code SQL plus court:

 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;
        }
    }
7
Jan Van der Haegen

Comme indiqué dans les réponses précédentes, lorsque vous souhaitez que votre IQueryable soit traduit en SQL, vous devez utiliser Expression au lieu de Func. Vous devez donc suivre la route de l'arbre d'expression.

Cependant, voici un moyen d’obtenir le même résultat sans avoir à construire vous-même l’arbre d’expression. Le truc, c'est que vous devez référencer LinqKit (disponible via NuGet) et appeler AsExpandable () dans la requête. Ceci s’occupera de construire l’arbre d’expression sous-jacent (voir comment ici ).

L'exemple ci-dessous utilise l'approche GroupJoin avec SelectMany et DefaultIfEmpty ():

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

Échantillon de données

Supposons que nous ayons les entités EF suivantes et que les variables utilisateurs _ et adresses sont l'accès au DbSet sous-jacent:

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

Rejoignons par identifiant:

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

Cela se traduit par (avec 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

Nous allons maintenant rejoindre plusieurs propriétés en utilisant un type anonyme comme clé:

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

Veuillez noter que les propriétés de type anonyme doivent avoir les mêmes noms, sinon vous obtiendrez une erreur de syntaxe.

C'est pourquoi nous avons Id = address.UserId au lieu de juste address.UserId.

Cela sera traduit en:

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
RaduV

C'est la méthode d'extension .LeftJoin que j'ai créée l'année dernière lorsque je voulais simplifier le .GroupJoin. J'ai eu de la chance avec ça. J'ai inclus les commentaires XML afin que vous obteniez l'intellisense complet. Il y a aussi une surcharge avec un IEqualityComparer. J'espère que tu trouves cela utile.

Ma suite complète des extensions de jointure est ici: 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>));
        }

    }
}
3
JohnnyIV

@Licentia, voici ce que j'ai proposé pour résoudre votre problème. J'ai créé les méthodes d'extension DynamicJoin et DynamicLeftJoin similaires à celles que vous m'avez montrées, mais j'ai traité la sortie différemment, car l'analyse de chaîne est vulnérable à de nombreux problèmes. Cela ne se joindra pas aux types anonymes, mais vous pouvez le modifier pour le faire. De plus, il n’a pas de surcharge pour IComparable, mais pourrait facilement être ajouté. Les noms de propriété doivent être casés de la même manière que le type. Ceci est utilisé dansconjonctionavec les méthodes d’extension ci-dessus (c’est-à-dire que cela ne fonctionnera pas sans elles). J'espère que ça aide!

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

Une mise à jour de ma réponse précédente. Lorsque je l'ai posté, je n'ai pas remarqué que la question concernait la traduction en SQL. Ce code fonctionne sur les éléments locaux, donc les objets seront d'abord extraits et ensuite joints au lieu d'effectuer la jointure externe sur le serveur. Mais pour gérer les valeurs NULL à l’aide des extensions Join j’avais posté plus tôt, voici un exemple:

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

}
0
JohnnyIV