LINQ - Join esterno completo


204

Ho un elenco di ID di persone e il loro nome e un elenco di ID di persone e il loro cognome. Alcune persone non hanno un nome e altre non hanno un cognome; Vorrei fare un join esterno completo nelle due liste.

Quindi i seguenti elenchi:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Dovrebbe produrre:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Sono nuovo di LINQ (quindi perdonami se sono zoppo) e ho trovato un bel po 'di soluzioni per "LINQ Outer Joins" che sembrano tutte abbastanza simili, ma sembrano davvero essere lasciati esterni.

I miei tentativi finora vanno qualcosa del genere:

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

Ma questo ritorna:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

Che cosa sto facendo di sbagliato?


2
È necessario che funzioni solo per gli elenchi in memoria o per Linq2Sql?
JamesFaix,

Risposte:


123

Non so se questo copre tutti i casi, logicamente sembra corretto. L'idea è di prendere un join esterno sinistro e un join esterno destro, quindi prendere l'unione dei risultati.

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

Funziona come scritto poiché è in LINQ to Objects. Se LINQ to SQL o altro, il Query Processor potrebbe non supportare la navigazione sicura o altre operazioni. Dovresti usare l'operatore condizionale per ottenere condizionalmente i valori.

vale a dire,

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };

2
L'Unione eliminerà i duplicati. Se non ti aspetti duplicati o puoi scrivere la seconda query per escludere qualsiasi cosa inclusa nella prima, usa invece Concat. Questa è la differenza SQL tra UNION e UNION ALL
cadrell0

3
@ cadre110 si verificheranno duplicati se una persona ha un nome e un cognome, quindi l'unione è una scelta valida.
Salsa

1
@saus ma c'è una colonna ID, quindi anche se c'è un nome e un cognome duplicati, l'ID dovrebbe essere diverso
cadrell0

1
La tua soluzione funziona per tipi primitivi, ma non sembra funzionare per gli oggetti. Nel mio caso, FirstName è un oggetto dominio, mentre LastName è un altro oggetto dominio. Quando unisco i due risultati, LINQ ha lanciato NotSupportedException (i tipi in Union o Concat sono costruiti in modo incompatibile). Hai avuto problemi simili?
Candy Chiu,

1
@CandyChiu: in realtà non mi sono mai imbattuto in un caso del genere. Immagino che sia una limitazione con il tuo provider di query. Probabilmente vorrai usare LINQ to Objects in quel caso chiamando AsEnumerable()prima di eseguire l'unione / concatenazione. Provalo e guarda come va. Se questa non è la strada che vuoi percorrere, non sono sicuro di poterti aiutare di più.
Jeff Mercado,

196

Aggiornamento 1: fornire un metodo di estensione veramente generalizzato FullOuterJoin
Aggiornamento 2: accettare facoltativamente un'abitudine IEqualityComparerper il tipo di chiave
Aggiornamento 3 : questa implementazione è recentemente diventata parte diMoreLinq - Grazie ragazzi!

Modifica aggiunta FullOuterGroupJoin( ideone ). Ho riutilizzato ilGetOuter<> implementazione, rendendola una frazione meno performante di quanto potrebbe essere, ma sto puntando a un codice "di alto livello", non ottimizzato al limite, proprio ora.

Guardalo dal vivo su http://ideone.com/O36nWc

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

Stampa l'output:

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

È inoltre possibile fornire le impostazioni predefinite: http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

Stampa:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

Spiegazione dei termini utilizzati:

Partecipare è un termine preso in prestito dalla progettazione di database relazionali:

  • Un join ripeterà gli elementi da aquante volte ci sono elementi b con il tasto corrispondente (ovvero: nulla se bfossero vuoti). Il gergo del database lo chiamainner (equi)join .
  • Un join esterno include elementi da acui non esiste alcun elemento corrispondenteb . (cioè: risultati pari se bfossero vuoti). Questo è di solito indicato comeleft join .
  • Un join esterno completo include record a sia dib se non esiste alcun elemento corrispondente nell'altro. (cioè anche i risultati se afossero vuoti)

Qualcosa no solito si vede in RDBMS è un join di gruppo [1] :

  • Un join di gruppo , fa lo stesso come descritto sopra, ma invece di ripetere gli elementi aper più corrispondenti b, raggruppa i record con le chiavi corrispondenti. Ciò è spesso più conveniente quando si desidera enumerare i record "uniti", in base a una chiave comune.

Guarda anche GroupJoin che contiene anche alcune spiegazioni generali.


[1] (Credo che Oracle e MSSQL abbiano estensioni proprietarie per questo)

Codice completo

Una classe di estensione 'drop-in' generalizzata per questo

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}

Modificato per mostrare l'utilizzo del FullOuterJoinmetodo di estensione fornito
vedere il

Modificato: aggiunto il metodo di estensione FullOuterGroupJoin
visto il

4
Invece di usare un dizionario, è possibile utilizzare un Lookup , che contiene la funzionalità espressa nei tuoi metodi di estensione di supporto. Ad esempio, puoi scrivere a.GroupBy(selectKeyA).ToDictionary();come a.ToLookup(selectKeyA)e adict.OuterGet(key)come alookup[key]. Ottenere la consegna delle chiavi è un po 'più complicato, però: alookup.Select(x => x.Keys).
Risky Martin

1
@RiskyMartin Grazie! Questo, in effetti, rende il tutto più elegante. Ho aggiornato la risposta e l'ideone. (Suppongo che le prestazioni dovrebbero essere aumentate poiché vengono istanziati meno oggetti).
vedi il

1
@Revious che funziona solo se sai che le chiavi sono uniche. E questo non è il caso comune di / grouping /. A parte questo, sì, sicuramente. Se sai che l'hash non trascinerà perf (i contenitori basati su nodo hanno più costi in linea di principio e l'hash non è gratuito e l'efficienza dipende dalla funzione hash / bucket bucket), sarà sicuramente più efficiente dal punto di vista algoritmico. Quindi, per piccoli carichi mi aspetto che potrebbe non essere più veloce
vedi il

27

Penso che ci siano problemi con la maggior parte di questi, inclusa la risposta accettata, perché non funzionano bene con Linq su IQueryable sia a causa di troppi viaggi di andata e ritorno sul server e di troppi ritorni di dati, o di esecuzione eccessiva del client.

Per IEnumerable non mi piace la risposta di Sehe o simile perché ha un uso eccessivo della memoria (un semplice test a due elenchi da 10000000 ha eseguito Linqpad con memoria insufficiente sul mio computer da 32 GB).

Inoltre, la maggior parte degli altri in realtà non implementa un vero Full Outer Join perché stanno usando un Union con un Right Right invece di Concat con un Right Anti Semi Join, che non solo elimina le duplicate righe di join interne dal risultato, ma eventuali duplicati propri che esistevano originariamente nei dati sinistro o destro.

Quindi ecco le mie estensioni che gestiscono tutti questi problemi, generano SQL e implementano direttamente il join in LINQ to SQL, eseguendolo sul server, ed è più veloce e con meno memoria rispetto ad altri su Enumerables:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLgR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

La differenza tra un Anti-semi-join destro è per lo più discutibile con Linq to Objects o nell'origine, ma fa la differenza sul lato server (SQL) nella risposta finale, rimuovendo un inutile JOIN.

La codifica manuale di Expressiongestire la fusione di un Expression<Func<>>in una lambda potrebbe essere migliorata con LinqKit, ma sarebbe bello se il linguaggio / compilatore avesse aggiunto qualche aiuto per questo. Le funzioni FullOuterJoinDistincte RightOuterJoinsono incluse per completezza, ma non ho FullOuterGroupJoinancora implementato nuovamente .

Ho scritto un'altra versione di un join esterno completo perIEnumerable per i casi in cui la chiave è ordinabile, che è circa il 50% più veloce rispetto alla combinazione del join esterno sinistro con l'anti semi-join destro, almeno su piccole raccolte. Passa attraverso ogni raccolta dopo l'ordinamento solo una volta.

Ho anche aggiunto un'altra risposta per una versione che funziona con EF sostituendo Invokecon un'espansione personalizzata.


Qual è il problema TP unusedP, TC unusedC? Sono letteralmente inutilizzati?
Rudey,

Sì, sono solo presenti per catturare i tipi in TP, TC, TResultper creare il giusto Expression<Func<>>. Dovrei Potrei sostituirlo con _, __, ___invece, ma che non sembra più chiaro fino a che C # ha un adeguato jolly parametro da utilizzare al posto.
NetMage

1
@MarcL. Non sono così sicuro di "noioso", ma sono d'accordo che questa risposta sia molto utile in questo contesto. Roba impressionante (anche se per me conferma le carenze di Linq-to-SQL)
vedi il

3
Sto ottenendo The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Ci sono restrizioni con questo codice? Voglio eseguire un JOIN COMPLETO su IQueryables
Learner,

1
Ho aggiunto una nuova risposta che sostituisce Invokeuna consuetudine ExpressionVisitorper in linea, Invokequindi dovrebbe funzionare con EF. Puoi provarlo?
NetMage

7

Ecco un metodo di estensione che lo fa:

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}

3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), che significa un join esterno completo = unione di unione esterna sinistra tutta l'unione esterna destra! Apprezzo la semplicità di questo approccio.
TamusJRoyce,

1
@TamusJRoyce Eccetto Unionrimuove i duplicati, quindi se ci sono righe duplicate nei dati originali, non saranno nel risultato.
NetMage

Ottimo punto! aggiungere un ID univoco se è necessario impedire la rimozione dei duplicati. Sì. L'unione è un po 'dispendiosa a meno che tu non possa accennare che esiste un ID univoco e l'unione passa al sindacato tutto (tramite euristica / ottimizzazioni interne). Ma funzionerà.
TamusJRoyce,


7

Immagino che l'approccio di @ sehe sia più forte, ma fino a quando non lo capirò meglio, mi ritrovo a saltare di scatto dall'estensione di @ MichaelSander. L'ho modificato per corrispondere alla sintassi e al tipo restituito del metodo incorporato Enumerable.Join () descritto qui . Ho aggiunto il suffisso "distinto" rispetto al commento di @ cadrell0 nella soluzione di @ JeffMercado.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

Nell'esempio, lo useresti in questo modo:

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

In futuro, mentre imparerò di più, ho la sensazione che migrerò alla logica di @ sehe data la sua popolarità. Ma anche allora dovrò stare attento, perché ritengo sia importante avere almeno un sovraccarico che corrisponda alla sintassi del metodo ".Join ()" esistente, se possibile, per due motivi:

  1. La coerenza nei metodi consente di risparmiare tempo, evitare errori ed evitare comportamenti involontari.
  2. Se ci sarà mai un metodo ".FullJoin ()" pronto per l'uso in futuro, immaginerei che cercherà di mantenere la sintassi del metodo ".Join ()" attualmente esistente, se possibile. In tal caso, se desideri migrare su di esso, puoi semplicemente rinominare le tue funzioni senza cambiare i parametri o preoccuparti di diversi tipi di ritorno che rompono il tuo codice.

Sono ancora nuovo con generici, estensioni, dichiarazioni Func e altre funzionalità, quindi il feedback è sicuramente benvenuto.

MODIFICARE: Non mi ci è voluto molto per capire che c'era un problema con il mio codice. Stavo facendo un .Dump () in LINQPad e guardando il tipo di ritorno. Era solo IEnumerable, quindi ho provato ad abbinarlo. Ma quando in realtà ho fatto un .Where () o .Select () sulla mia estensione ho ricevuto un errore: "'System Collections.IEnumerable' non contiene una definizione per 'Select' e ...". Quindi alla fine sono stato in grado di abbinare la sintassi di input di .Join (), ma non il comportamento di ritorno.

EDIT: aggiunto "TResult" al tipo restituito per la funzione. Perso che leggendo l'articolo di Microsoft, e ovviamente ha senso. Con questa correzione, ora sembra che il comportamento di ritorno sia in linea con i miei obiettivi dopo tutto.


+2 per questa risposta e per Michael Sanders. Ho accidentalmente cliccato su questo e il voto è bloccato. Per favore aggiungine due.
TamusJRoyce,

@TamusJRoyce, sono appena entrato per modificare un po 'i formati del codice. Credo che dopo aver apportato una modifica, hai la possibilità di rifondere il tuo voto. Prova, se vuoi.
pwilcox,

Grazie mille!
Roshna Omer,

6

Come hai scoperto, Linq non ha un costrutto "join esterno". Il più vicino che puoi ottenere è un join esterno sinistro usando la query che hai dichiarato. A questo, puoi aggiungere tutti gli elementi dell'elenco dei cognomi che non sono rappresentati nel join:

outerJoin = outerJoin.Concat(lastNames.Select(l=>new
                            {
                                id = l.ID,
                                firstname = String.Empty,
                                surname = l.Name
                            }).Where(l=>!outerJoin.Any(o=>o.id == l.id)));

2

Mi piace la risposta di sehe, ma non usa l'esecuzione differita (le sequenze di input sono enumerate con entusiasmo dalle chiamate a ToLookup). Quindi, dopo aver esaminato i sorgenti .NET per LINQ-to-objects , ho pensato a questo:

public static class LinqExtensions
{
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator = null,
        TLeft defaultLeft = default(TLeft),
        TRight defaultRight = default(TRight))
    {
        if (left == null) throw new ArgumentNullException("left");
        if (right == null) throw new ArgumentNullException("right");
        if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
        if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        comparator = comparator ?? EqualityComparer<TKey>.Default;
        return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
    }

    internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator,
        TLeft defaultLeft,
        TRight defaultRight)
    {
        var leftLookup = left.ToLookup(leftKeySelector, comparator);
        var rightLookup = right.ToLookup(rightKeySelector, comparator);
        var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);

        foreach (var key in keys)
            foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
                foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
                    yield return resultSelector(leftValue, rightValue, key);
    }
}

Questa implementazione ha le seguenti proprietà importanti:

  • Esecuzione posticipata, le sequenze di input non verranno enumerate prima di enumerare la sequenza di output.
  • Enumera le sequenze di input solo una volta ciascuna.
  • Conserva l'ordine delle sequenze di input, nel senso che produrrà tuple nell'ordine della sequenza sinistra e poi della destra (per i tasti non presenti nella sequenza sinistra).

Queste proprietà sono importanti, perché si aspettano da qualcuno che non conosce FullOuterJoin ma che ha esperienza con LINQ.


Non mantiene l'ordine delle sequenze di input: la ricerca non lo garantisce, quindi queste foreach verranno enumerate in un ordine del lato sinistro, quindi un ordine del lato destro non presente nel lato sinistro. Ma l'ordine relazionale degli elementi non viene preservato.
Ivan Danilov,

@IvanDanilov Hai ragione sul fatto che questo non è in realtà nel contratto. L'implementazione di ToLookup, tuttavia, utilizza una classe di ricerca interna in Enumerable.cs che mantiene i raggruppamenti in un elenco collegato ordinato per inserzione e utilizza questo elenco per scorrere attraverso di essi. Quindi, nella versione corrente di .NET, l'ordine è garantito, ma poiché MS purtroppo non lo ha documentato, potrebbero cambiarlo nelle versioni successive.
Søren Boisen,

L'ho provato su .NET 4.5.1 su Win 8.1 e non conserva l'ordine.
Ivan Danilov,

1
"... le sequenze di input sono enumerate con entusiasmo dalle chiamate a ToLookup". Ma l'implementazione fa esattamente lo stesso. La resa non dà molto qui a causa delle spese per la macchina a stati finiti.
pkuderov,

4
Le chiamate di ricerca vengono eseguite quando viene richiesto il primo elemento del risultato e non quando viene creato l'iteratore. Questo è ciò che significa l'esecuzione differita. È possibile rinviare ulteriormente l'enumerazione di un set di input, ripetendo direttamente l'Enumerable di sinistra invece di convertirlo in una Ricerca, con il vantaggio aggiuntivo di preservare l'ordine del set di sinistra.
Rolf

2

Ho deciso di aggiungere questa come risposta separata in quanto non sono sicuro che sia stato testato abbastanza. Questa è una reimplementazione diFullOuterJoin metodo utilizzando essenzialmente una versione semplificata e personalizzata di LINQKit Invoke/ Expandfor Expressionaffinché funzioni Entity Framework. Non c'è molta spiegazione in quanto è praticamente la stessa della mia risposta precedente.

public static class Ext {
    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg");
        var parmC = Expression.Parameter(typeof(TRight), "r");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr");
        var parmC = Expression.Parameter(typeof(TLeft), "l");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right })
                         .SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    private static Expression<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // newrightrs = lgr => resultSelector(default(TLeft), lgr.right)
        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(resultSelector.Apply(argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector)  where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Cast<ParameterExpression>().Zip(args, (p, a) => (p, a))) {
            b = b.Replace(pa.p, pa.a);
        }

        return b.PropagateNull();
    }

    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
    public class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor {
        public readonly Expression from;
        public readonly Expression to;

        public ReplaceVisitor(Expression _from, Expression _to) {
            from = _from;
            to = _to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);
    public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
                return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
            else
                return base.Visit(node);
        }
    }

    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }
}

NetMage, codifica impressionante! Quando lo eseguo con un semplice esempio e quando [NullVisitor.Visit (..) viene invocato in [base.Visit (Node)], viene generato un [System.ArgumentException: I tipi di argomento non corrispondono]. Il che è vero, poiché sto usando un [Guid] TKey e ad un certo punto il visitatore null si aspetta un tipo [Guid?]. Forse mi sto perdendo qualcosa. Ho un breve esempio codificato per EF 6.4.4. Per favore fatemi sapere come posso condividere questo codice con voi. Grazie!
Troncho,

@Troncho Normalmente utilizzo LINQPad per i test, quindi EF 6 non è facile da eseguire. base.Visit(node)non dovresti lanciare un'eccezione in quanto si limita a ricorrere all'albero. Posso accedere praticamente a qualsiasi servizio di condivisione del codice, ma non impostare un database di prova. L'esecuzione sul mio test LINQ to SQL sembra funzionare bene, però.
NetMage

@Troncho È possibile che ti unisca tra una Guidchiave e una Guid?chiave esterna?
NetMage

Sto usando LinqPad anche per i test. La mia query ha generato ArgumentException, quindi ho deciso di eseguire il debug su VS2019 su [.Net Framework 4.7.1] e sull'ultimo EF 6. Lì ho avuto modo di rintracciare il vero problema. Per testare il tuo codice, sto generando 2 set di dati separati provenienti dalla stessa tabella [Persone]. Filtra entrambi i set in modo che alcuni record siano univoci per ciascun set e alcuni esistano su entrambi i set. [PersonId] è una [Chiave primaria] Guid (c #) / Uniqueidentifier (SqlServer) e nessuno dei due genera un valore [PersonId] nullo. Codice condiviso: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Esegue un'enumerazione di streaming in memoria su entrambi gli input e richiama il selettore per ogni riga. Se non esiste alcuna correlazione nell'iterazione corrente, uno degli argomenti del selettore sarà nullo .

Esempio:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Richiede un IComparer per il tipo di correlazione, utilizza Comparer.Default se non fornito.

  • Richiede che "OrderBy" sia applicato agli enumerabili di input

    /// <summary>
    /// Performs a full outer join on two <see cref="IEnumerable{T}" />.
    /// </summary>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <param name="leftKeySelector"></param>
    /// <param name="rightKeySelector"></param>
    /// <param name="selector">Expression defining result type</param>
    /// <param name="keyComparer">A comparer if there is no default for the type</param>
    /// <returns></returns>
    [System.Diagnostics.DebuggerStepThrough]
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TValue> leftKeySelector,
        Func<TRight, TValue> rightKeySelector,
        Func<TLeft, TRight, TResult> selector,
        IComparer<TValue> keyComparer = null)
        where TLeft: class
        where TRight: class
        where TValue : IComparable
    {
    
        keyComparer = keyComparer ?? Comparer<TValue>.Default;
    
        using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator())
        using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator())
        {
    
            var hasLeft = enumLeft.MoveNext();
            var hasRight = enumRight.MoveNext();
            while (hasLeft || hasRight)
            {
    
                var currentLeft = enumLeft.Current;
                var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue);
    
                var currentRight = enumRight.Current;
                var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue);
    
                int compare =
                    !hasLeft ? 1
                    : !hasRight ? -1
                    : keyComparer.Compare(valueLeft, valueRight);
    
                switch (compare)
                {
                    case 0:
                        // The selector matches. An inner join is achieved
                        yield return selector(currentLeft, currentRight);
                        hasLeft = enumLeft.MoveNext();
                        hasRight = enumRight.MoveNext();
                        break;
                    case -1:
                        yield return selector(currentLeft, default(TRight));
                        hasLeft = enumLeft.MoveNext();
                        break;
                    case 1:
                        yield return selector(default(TLeft), currentRight);
                        hasRight = enumRight.MoveNext();
                        break;
                }
            }
    
        }
    
    }

1
È uno sforzo eroico per rendere le cose "in streaming". Purtroppo, tutto il guadagno viene perso al primo passo, dove ci si esibisce OrderBysu entrambe le proiezioni chiave. OrderBybuffer l'intera sequenza, per ovvi motivi .
visto il

@sehe Hai sicuramente ragione per Linq to Objects. Se IEnumerable <T> è IQueryable <T>, l'origine dovrebbe ordinare, ma non c'è tempo per testarlo. Se mi sbaglio, sostituire semplicemente l'input IEnumerable <T> con IQueryable <T> dovrebbe ordinare nel sorgente / database.
James Caradoc-Davies,

1

La mia soluzione pulita per la situazione che chiave è unica in entrambi gli enumerabili:

 private static IEnumerable<TResult> FullOuterJoin<Ta, Tb, TKey, TResult>(
            IEnumerable<Ta> a, IEnumerable<Tb> b,
            Func<Ta, TKey> key_a, Func<Tb, TKey> key_b,
            Func<Ta, Tb, TResult> selector)
        {
            var alookup = a.ToLookup(key_a);
            var blookup = b.ToLookup(key_b);
            var keys = new HashSet<TKey>(alookup.Select(p => p.Key));
            keys.UnionWith(blookup.Select(p => p.Key));
            return keys.Select(key => selector(alookup[key].FirstOrDefault(), blookup[key].FirstOrDefault()));
        }

così

    var ax = new[] {
        new { id = 1, first_name = "ali" },
        new { id = 2, first_name = "mohammad" } };
    var bx = new[] {
        new { id = 1, last_name = "rezaei" },
        new { id = 3, last_name = "kazemi" } };

    var list = FullOuterJoin(ax, bx, a => a.id, b => b.id, (a, b) => "f: " + a?.first_name + " l: " + b?.last_name).ToArray();

uscite:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi

0

Join esterno completo per due o più tabelle: per prima cosa estrarre la colonna su cui si desidera unirsi.

var DatesA = from A in db.T1 select A.Date; 
var DatesB = from B in db.T2 select B.Date; 
var DatesC = from C in db.T3 select C.Date;            

var Dates = DatesA.Union(DatesB).Union(DatesC); 

Quindi utilizzare il join esterno sinistro tra la colonna estratta e le tabelle principali.

var Full_Outer_Join =

(from A in Dates
join B in db.T1
on A equals B.Date into AB 

from ab in AB.DefaultIfEmpty()
join C in db.T2
on A equals C.Date into ABC 

from abc in ABC.DefaultIfEmpty()
join D in db.T3
on A equals D.Date into ABCD

from abcd in ABCD.DefaultIfEmpty() 
select new { A, ab, abc, abcd })
.AsEnumerable();

0

Ho scritto questa classe di estensioni per un'app forse 6 anni fa e la uso da allora in molte soluzioni senza problemi. Spero che sia d'aiuto.

modifica: ho notato che alcuni potrebbero non sapere come utilizzare una classe di estensione.

Per usare questa classe di estensione, fai semplicemente riferimento al suo spazio dei nomi nella tua classe aggiungendo la seguente riga usando joinext;

^ questo dovrebbe permetterti di vedere l'intellisense delle funzioni di estensione su qualsiasi collezione di oggetti IEnumerable che usi per caso.

Spero che questo ti aiuti. Fammi sapere se non è ancora chiaro e spero di scrivere un esempio di esempio su come usarlo.

Ora ecco la classe:

namespace joinext
{    
public static class JoinExtensions
    {
        public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
            where TInner : class
            where TOuter : class
        {
            var innerLookup = inner.ToLookup(innerKeySelector);
            var outerLookup = outer.ToLookup(outerKeySelector);

            var innerJoinItems = inner
                .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem)))
                .Select(innerItem => resultSelector(null, innerItem));

            return outer
                .SelectMany(outerItem =>
                {
                    var innerItems = innerLookup[outerKeySelector(outerItem)];

                    return innerItems.Any() ? innerItems : new TInner[] { null };
                }, resultSelector)
                .Concat(innerJoinItems);
        }


        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.GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (o, i) =>
                    new { o = o, i = i.DefaultIfEmpty() })
                    .SelectMany(m => m.i.Select(inn =>
                        resultSelector(m.o, inn)
                        ));

        }



        public static IEnumerable<TResult> RightJoin<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 inner.GroupJoin(
                outer,
                innerKeySelector,
                outerKeySelector,
                (i, o) =>
                    new { i = i, o = o.DefaultIfEmpty() })
                    .SelectMany(m => m.o.Select(outt =>
                        resultSelector(outt, m.i)
                        ));

        }

    }
}

1
Sfortunatamente, sembra che la funzione in SelectManynon possa essere convertita in un albero di espressioni degno di LINQ2SQL, a quanto pare.
OR Mapper,

edc65. So che potrebbe essere una domanda sciocca se lo hai già fatto. Ma nel caso (come ho notato alcuni non lo sanno), devi solo fare riferimento allo spazio dei nomi joinext.
H7O,

O Mapper, fammi sapere con quale tipo di raccolta vuoi che funzioni. Dovrebbe funzionare benissimo con qualsiasi collezione IEnumerable
H7O,

0

Penso che la clausola di join LINQ non sia la soluzione corretta a questo problema, poiché lo scopo della clausola di join non è quello di accumulare dati nel modo richiesto per questa soluzione di attività. Il codice per unire raccolte separate create diventa troppo complicato, forse è OK per scopi di apprendimento, ma non per applicazioni reali. Uno dei modi in cui risolvere questo problema è nel codice seguente:

class Program
{
    static void Main(string[] args)
    {
        List<FirstName> firstNames = new List<FirstName>();
        firstNames.Add(new FirstName { ID = 1, Name = "John" });
        firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

        List<LastName> lastNames = new List<LastName>();
        lastNames.Add(new LastName { ID = 1, Name = "Doe" });
        lastNames.Add(new LastName { ID = 3, Name = "Smith" });

        HashSet<int> ids = new HashSet<int>();
        foreach (var name in firstNames)
        {
            ids.Add(name.ID);
        }
        foreach (var name in lastNames)
        {
            ids.Add(name.ID);
        }
        List<FullName> fullNames = new List<FullName>();
        foreach (int id in ids)
        {
            FullName fullName = new FullName();
            fullName.ID = id;
            FirstName firstName = firstNames.Find(f => f.ID == id);
            fullName.FirstName = firstName != null ? firstName.Name : string.Empty;
            LastName lastName = lastNames.Find(l => l.ID == id);
            fullName.LastName = lastName != null ? lastName.Name : string.Empty;
            fullNames.Add(fullName);
        }
    }
}
public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}
class FullName
{
    public int ID;

    public string FirstName;

    public string LastName;
}

Se le raccolte reali sono grandi per la formazione di HashSet invece per ogni ciclo, è possibile utilizzare il codice seguente:

List<int> firstIds = firstNames.Select(f => f.ID).ToList();
List<int> LastIds = lastNames.Select(l => l.ID).ToList();
HashSet<int> ids = new HashSet<int>(firstIds.Union(LastIds));//Only unique IDs will be included in HashSet

0

Grazie a tutti per i post interessanti!

Ho modificato il codice perché nel mio caso avevo bisogno

  • un predicato di join personalizzato
  • un comparatore distinto unione personalizzata

Per quelli interessati questo è il mio codice modificato (in VB, scusa)

    Module MyExtensions
        <Extension()>
        Friend Function FullOuterJoin(Of TA, TB, TResult)(ByVal a As IEnumerable(Of TA), ByVal b As IEnumerable(Of TB), ByVal joinPredicate As Func(Of TA, TB, Boolean), ByVal projection As Func(Of TA, TB, TResult), ByVal comparer As IEqualityComparer(Of TResult)) As IEnumerable(Of TResult)
            Dim joinL =
                From xa In a
                From xb In b.Where(Function(x) joinPredicate(xa, x)).DefaultIfEmpty()
                Select projection(xa, xb)
            Dim joinR =
                From xb In b
                From xa In a.Where(Function(x) joinPredicate(x, xb)).DefaultIfEmpty()
                Select projection(xa, xb)
            Return joinL.Union(joinR, comparer)
        End Function
    End Module

    Dim fullOuterJoin = lefts.FullOuterJoin(
        rights,
        Function(left, right) left.Code = right.Code And (left.Amount [...] Or left.Description.Contains [...]),
        Function(left, right) New CompareResult(left, right),
        New MyEqualityComparer
    )

    Public Class MyEqualityComparer
        Implements IEqualityComparer(Of CompareResult)

        Private Function GetMsg(obj As CompareResult) As String
            Dim msg As String = ""
            msg &= obj.Code & "_"
            [...]
            Return msg
        End Function

        Public Overloads Function Equals(x As CompareResult, y As CompareResult) As Boolean Implements IEqualityComparer(Of CompareResult).Equals
            Return Me.GetMsg(x) = Me.GetMsg(y)
        End Function

        Public Overloads Function GetHashCode(obj As CompareResult) As Integer Implements IEqualityComparer(Of CompareResult).GetHashCode
            Return Me.GetMsg(obj).GetHashCode
        End Function
    End Class

0

Ancora un altro join esterno completo

Non essendo così contento della semplicità e della leggibilità delle altre proposizioni, ho finito con questo:

Non ha la pretesa di essere veloce (circa 800 ms per unire 1000 * 1000 su una CPU 2020m: 2.4ghz / 2cores). Per me, è solo un join esterno completo compatto e casual.

Funziona come un SQL FULL OUTER JOIN (conservazione dei duplicati)

Saluti ;-)

using System;
using System.Collections.Generic;
using System.Linq;
namespace NS
{
public static class DataReunion
{
    public static List<Tuple<T1, T2>> FullJoin<T1, T2, TKey>(List<T1> List1, Func<T1, TKey> KeyFunc1, List<T2> List2, Func<T2, TKey> KeyFunc2)
    {
        List<Tuple<T1, T2>> result = new List<Tuple<T1, T2>>();

        Tuple<TKey, T1>[] identifiedList1 = List1.Select(_ => Tuple.Create(KeyFunc1(_), _)).OrderBy(_ => _.Item1).ToArray();
        Tuple<TKey, T2>[] identifiedList2 = List2.Select(_ => Tuple.Create(KeyFunc2(_), _)).OrderBy(_ => _.Item1).ToArray();

        identifiedList1.Where(_ => !identifiedList2.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(_.Item2, default(T2)));
        });

        result.AddRange(
            identifiedList1.Join(identifiedList2, left => left.Item1, right => right.Item1, (left, right) => Tuple.Create<T1, T2>(left.Item2, right.Item2)).ToList()
        );

        identifiedList2.Where(_ => !identifiedList1.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(default(T1), _.Item2));
        });

        return result;
    }
}
}

L'idea è di

  1. Crea ID in base ai costruttori di funzioni chiave fornite
  2. Il processo ha lasciato solo articoli
  3. Elabora join interno
  4. Elabora solo gli articoli giusti

Ecco un breve test che lo accompagna:

Posizionare un punto di interruzione alla fine per verificare manualmente che si comporti come previsto

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NS;

namespace Tests
{
[TestClass]
public class DataReunionTest
{
    [TestMethod]
    public void Test()
    {
        List<Tuple<Int32, Int32, String>> A = new List<Tuple<Int32, Int32, String>>();
        List<Tuple<Int32, Int32, String>> B = new List<Tuple<Int32, Int32, String>>();

        Random rnd = new Random();

        /* Comment the testing block you do not want to run
        /* Solution to test a wide range of keys*/

        for (int i = 0; i < 500; i += 1)
        {
            A.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "A"));
            B.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "B"));
        }

        /* Solution for essential testing*/

        A.Add(Tuple.Create(1, 2, "B11"));
        A.Add(Tuple.Create(1, 2, "B12"));
        A.Add(Tuple.Create(1, 3, "C11"));
        A.Add(Tuple.Create(1, 3, "C12"));
        A.Add(Tuple.Create(1, 3, "C13"));
        A.Add(Tuple.Create(1, 4, "D1"));

        B.Add(Tuple.Create(1, 1, "A21"));
        B.Add(Tuple.Create(1, 1, "A22"));
        B.Add(Tuple.Create(1, 1, "A23"));
        B.Add(Tuple.Create(1, 2, "B21"));
        B.Add(Tuple.Create(1, 2, "B22"));
        B.Add(Tuple.Create(1, 2, "B23"));
        B.Add(Tuple.Create(1, 3, "C2"));
        B.Add(Tuple.Create(1, 5, "E2"));

        Func<Tuple<Int32, Int32, String>, Tuple<Int32, Int32>> key = (_) => Tuple.Create(_.Item1, _.Item2);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        var res = DataReunion.FullJoin(A, key, B, key);
        watch.Stop();
        var elapsedMs = watch.ElapsedMilliseconds;
        String aser = JToken.FromObject(res).ToString(Formatting.Indented);
        Console.Write(elapsedMs);
    }
}

}


-4

Odio davvero queste espressioni linq, ecco perché esiste SQL:

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname
   from firstnames fn
   full join lastnames ln on ln.id=fn.id

Creare questo come vista sql nel database e importarlo come entità.

Certo, anche l'unione (distinta) dei giunti sinistro e destro lo farà, ma è stupido.


11
Perché non rilasciare quante più astrazioni possibili e farlo nel codice macchina? (Suggerimento: perché le astrazioni di ordine superiore semplificano la vita del programmatore). Questo non risponde alla domanda e mi sembra più un rant contro LINQ.
spender il

8
Chi ha detto che i dati provengono da un database?
user247702

1
Certo, si tratta di un database, ci sono parole "join esterno" in questione :) google.cz/search?q=outer+join
Milano Švec,

1
Capisco che si tratta di una soluzione "vecchio stile", ma prima di effettuare il downgrade, confrontare la sua complessità con altre soluzioni :) Tranne quella accettata, è ovviamente quella corretta.
Milano Švec,

Ovviamente può essere un database o no. Sto cercando una soluzione con un join esterno tra gli elenchi in memoria
edc65
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.