LINQ - Junção externa completa

202

Eu tenho uma lista do ID das pessoas e seu primeiro nome, e uma lista do ID das pessoas e seu sobrenome. Algumas pessoas não têm nome e outras não, sobrenome; Eu gostaria de fazer uma junção externa completa nas duas listas.

Portanto, as seguintes listas:

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

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

Deve produzir:

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

Eu sou novo no LINQ (então, desculpe-me se estiver sendo manco) e encontrei algumas soluções para 'LINQ Outer Joins' que parecem muito semelhantes, mas realmente parecem ser uniões externas.

Minhas tentativas até agora são mais ou menos assim:

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

Mas isso retorna:

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

O que estou fazendo de errado?

ninjaPixel
fonte
2
Você precisa que isso funcione apenas para listas na memória ou para o Linq2Sql?
JamesFaix
Tente .GroupJoin () stackoverflow.com/questions/15595289/…
jdev.ninja 3/03

Respostas:

122

Não sei se isso cobre todos os casos, logicamente parece correto. A idéia é criar uma junção externa esquerda e externa direita e, em seguida, realizar a união dos resultados.

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

Isso funciona como está escrito, pois está no LINQ to Objects. Se LINQ to SQL ou outro, o processador de consultas pode não suportar navegação segura ou outras operações. Você precisaria usar o operador condicional para obter condicionalmente os valores.

ou seja,

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,
    };
Jeff Mercado
fonte
2
União eliminará duplicatas. Se você não espera duplicatas ou pode escrever a segunda consulta para excluir qualquer coisa que foi incluída na primeira, use o Concat. Esta é a diferença entre SQL UNION e UNION ALL
cadrell0
3
@ cadre110 duplicatas ocorrerão se uma pessoa tiver um nome e um sobrenome, então a união é uma opção válida.
saus
1
@saus mas há uma coluna de ID, por isso mesmo se houver um primeiro e último nome duplicado, o ID deve ser diferente
cadrell0
1
Sua solução funciona para tipos primitivos, mas parece não funcionar para objetos. No meu caso, FirstName é um objeto de domínio, enquanto LastName é outro objeto de domínio. Quando uno os dois resultados, o LINQ lançou uma NotSupportedException (tipos no Union ou Concat são construídos de forma incompatível). Você já teve problemas semelhantes?
quer
1
@CandyChiu: Na verdade, eu nunca tive esse caso. Eu acho que isso é uma limitação com seu provedor de consultas. Você provavelmente desejará usar o LINQ to Objects nesse caso chamando AsEnumerable()antes de executar a união / concatenação. Tente isso e veja como isso acontece. Se esse não é o caminho que você deseja seguir, não tenho certeza se posso ajudar mais do que isso.
Jeff Mercado
196

Atualização 1: fornecendo um método de extensão verdadeiramente generalizado FullOuterJoin
Atualização 2: opcionalmente aceitando um personalizado IEqualityComparerpara o tipo de chave
Atualização 3 : esta implementação recentemente se tornou parte deMoreLinq - Obrigado pessoal!

Editar Adicionado FullOuterGroupJoin( ideone ). Reutilizei a GetOuter<>implementação, tornando esta uma fração menos eficiente do que poderia ser, mas estou buscando um código de 'alto nível', não otimizado de ponta, agora.

Veja ao vivo em 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);
}

Imprime a saída:

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

Você também pode fornecer os padrões: 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)" }
        )

Impressão:

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

Explicação dos termos utilizados:

União é um termo emprestado do design de banco de dados relacional:

  • Uma junção repetirá elementos aquantas vezes houver elementos b com a chave correspondente (ou seja: nada se estiver bvazio). A linguagem do banco de dados chama issoinner (equi)join .
  • Uma junção externa inclui elementos dos aquais não existe nenhum elemento correspondenteb . (ou seja: resultados pares se bestiverem vazios). Isso geralmente é chamado deleft join .
  • Uma junção externa completa inclui registros a eb se nenhum elemento correspondente existe no outro. (ou seja, resultados iguais se aestivessem vazios)

Algo geralmente não visto no RDBMS é uma junção de grupo [1] :

  • Uma junção de grupo faz o mesmo descrito acima, mas , em vez de repetir elementos de avários correspondentes b, agrupa os registros com as chaves correspondentes. Isso geralmente é mais conveniente quando você deseja enumerar os registros 'unidos', com base em uma chave comum.

Consulte também GroupJoin, que também contém algumas explicações gerais.


[1] (acredito que Oracle e MSSQL têm extensões proprietárias para isso)

Código completo

Uma classe de extensão 'drop-in' generalizada para este

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;
    }
}
ver
fonte
Editado para mostrar o uso do FullOuterJoinmétodo de extensão fornecido
sehe
Editado: FullOuterGroupJoin método de extensão adicionado
sehe
4
Em vez de usar um dicionário, você pode usar uma pesquisa , que contém a funcionalidade expressa nos métodos de extensão auxiliar. Por exemplo, você pode escrever a.GroupBy(selectKeyA).ToDictionary();como a.ToLookup(selectKeyA)e adict.OuterGet(key)como alookup[key]. Obtendo a coleção de chaves é um pouco mais complicado, no entanto: alookup.Select(x => x.Keys).
Risky Martin
1
@RiskyMartin Thanks! Isso, de fato, torna a coisa toda mais elegante. Eu atualizei a resposta e os ideone-s. (Suponho que o desempenho deva ser aumentado, pois menos objetos são instanciados).
precisa
1
@ Anterior que funciona apenas se você souber que as chaves são únicas. E esse não é o caso comum para / agrupamento /. Fora isso, sim, por todos os meios. Se você sabe que o hash não vai arrastar o perf (os contêineres baseados em nós têm mais custos em princípio, e o hash não é gratuito e a eficiência depende da função de hash / spread do bucket), certamente será mais eficiente em termos de algoritmos. Assim, para pequenas cargas eu esperaria que não poderia ser mais rápido
sehe
27

Eu acho que há problemas com a maioria deles, incluindo a resposta aceita, porque eles não funcionam bem com o Linq sobre o IQueryable devido a muitas viagens de ida e volta ao servidor e a muitos retornos de dados, ou a muita execução do cliente.

Para IEnumerable, não gosto da resposta de Sehe ou similar, porque ela tem uso excessivo de memória (um simples teste de 10000000 com duas listas executou o Linqpad sem memória na minha máquina de 32 GB).

Além disso, a maioria dos outros não implementa uma Junção Externa Completa adequada porque eles estão usando uma União com uma Junta Direita em vez de Concat com uma Junta Semi Semi Direita, o que não apenas elimina as linhas de junção interna duplicadas do resultado, mas também quaisquer duplicatas adequadas que existiam originalmente nos dados esquerdo ou direito.

Então, aqui estão minhas extensões que lidam com todos esses problemas, geram SQL e implementam a junção no LINQ to SQL diretamente, executando no servidor, e são mais rápidas e com menos memória que outras no 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));
    }
}

A diferença entre uma Anti-Semi-Join Direita é principalmente discutida com o Linq to Objects ou na fonte, mas faz a diferença no lado do servidor (SQL) na resposta final, removendo uma desnecessária JOIN.

A codificação manual Expressionpara lidar com a fusão de um Expression<Func<>>em um lambda poderia ser melhorada com o LinqKit, mas seria bom se o idioma / compilador tivesse adicionado alguma ajuda para isso. As funções FullOuterJoinDistincte RightOuterJoinestão incluídas para garantir a integridade, mas ainda não as implementei FullOuterGroupJoin.

Eu escrevi outra versão de uma junção externa completa IEnumerablepara casos em que a chave pode ser solicitada, que é cerca de 50% mais rápida do que a combinação da junção externa esquerda com a anti junção anti certa, pelo menos em coleções pequenas. Ele percorre cada coleção após classificar apenas uma vez.

Também adicionei outra resposta para uma versão que funciona com a EF, substituindo a Invokepor uma expansão personalizada.

NetMage
fonte
Qual é o problema TP unusedP, TC unusedC? Eles estão literalmente sem uso?
Rudey
Sim, eles são apenas presente para capturar os tipos em TP, TC, TResultpara criar o bom Expression<Func<>>. Eu acho que eu poderia substituí-los com _, __, ___em vez disso, mas isso não parece mais claro até que C # tem um curinga parâmetro adequado para usar em vez.
NetMage 7/07
1
@MarcL. Não tenho tanta certeza sobre 'cansativo' - mas concordo que essa resposta é muito útil nesse contexto. Coisas impressionantes (embora, para mim, confirme as deficiências do Linq-to-SQL)
sehe
3
Eu estou recebendo The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Há alguma restrição com este código? Eu quero executar uma junção completa sobre IQueryables
aprendiz
1
Eu adicionei uma nova resposta que substitui Invokepor um personalizado ExpressionVisitorpara incorporar o, de Invokemodo que ele funcione com a EF. Você pode tentar?
NetMage 22/0318
7

Aqui está um método de extensão fazendo isso:

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));
}
Michael Sander
fonte
3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), o que significa uma junção externa completa = união de junção externa esquerda toda junção externa direita! Agradeço a simplicidade dessa abordagem.
TamusJRoyce
1
@TamusJRoyce Exceto Unionremove duplicatas, portanto, se houver linhas duplicadas nos dados originais, elas não estarão no resultado.
NetMage 31/01/19
Ótimo ponto! adicione um ID exclusivo se precisar impedir a remoção de duplicatas. Sim. A união é um pouco inútil, a menos que você possa sugerir que existe um ID exclusivo e a união alterna para união de todas (por meio de heurísticas / otimizações internas). Mas vai funcionar.
TamusJRoyce 01/02/19
O mesmo que a resposta aceita .
Gert Arnold
7

Acho que a abordagem da @sehe é mais forte, mas até que eu a entenda melhor, eu me pego pulando na extensão do @ MichaelSander. Modifiquei-o para corresponder à sintaxe e ao tipo de retorno do método interno Enumerable.Join () descrito aqui . Anexei o sufixo "distinto" em relação ao comentário de @ cadrell0 na solução de @ 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);
    }

}

No exemplo, você usaria assim:

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

No futuro, à medida que aprender mais, tenho a sensação de que migrarei para a lógica da @ sehe, dada sua popularidade. Mas, mesmo assim, terei que ter cuidado, porque sinto que é importante ter pelo menos uma sobrecarga que corresponda à sintaxe do método ".Join ()" existente, se possível, por dois motivos:

  1. A consistência nos métodos ajuda a economizar tempo, evitar erros e evitar comportamentos não intencionais.
  2. Se algum dia houver um método ".FullJoin ()" pronto para uso no futuro, eu imagino que ele tentará manter a sintaxe do método ".Join ()" atualmente existente, se puder. Se isso acontecer, se você quiser migrar para ele, basta renomear suas funções sem alterar os parâmetros ou se preocupar com os diferentes tipos de retorno que quebram seu código.

Ainda sou novo com genéricos, extensões, instruções Func e outros recursos, portanto o feedback é certamente bem-vindo.

Edição: não demorou muito tempo para perceber que havia um problema com o meu código. Eu estava fazendo um .Dump () no LINQPad e olhando para o tipo de retorno. Era apenas IEnumerable, então tentei combinar. Mas quando eu realmente fiz um .Where () ou .Select () na minha extensão, recebi um erro: "'System Collections.IEnumerable' não contém uma definição para 'Select' e ...". Portanto, no final, consegui corresponder à sintaxe de entrada de .Join (), mas não ao comportamento de retorno.

EDIT: Adicionado "TResult" ao tipo de retorno para a função. Perdeu isso ao ler o artigo da Microsoft, e é claro que faz sentido. Com essa correção, agora parece que o comportamento do retorno está alinhado com meus objetivos, afinal.

pwilcox
fonte
+2 para esta resposta, assim como Michael Sanders. Eu acidentalmente cliquei aqui e a votação está bloqueada. Por favor, adicione dois.
TamusJRoyce
@TamusJRoyce, acabei de editar um pouco os formatos de código. Acredito que depois que uma edição é feita, você tem a opção de reformular seu voto. Experimente, se quiser.
Pwilcox
Muito obrigado!
Roshna Omer
6

Como você descobriu, o Linq não possui uma construção "junção externa". O mais próximo que você pode obter é uma junção externa esquerda usando a consulta que você declarou. Para isso, você pode adicionar qualquer elemento da lista de sobrenomes que não esteja representado na junção:

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)));
KeithS
fonte
2

Gosto da resposta de ela, mas ela não usa execução adiada (as seqüências de entrada são ansiosamente enumeradas pelas chamadas para ToLookup). Então, depois de examinar as fontes .NET para LINQ-to-objects , vim com isso:

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

Esta implementação possui as seguintes propriedades importantes:

  • Adiada execução, as seqüências de entrada não serão enumeradas antes da sequência de saída ser enumerada.
  • Enumera apenas as seqüências de entrada uma vez cada.
  • Preserva a ordem das sequências de entrada, no sentido em que produzirá tuplas na ordem da sequência esquerda e depois na direita (para as teclas que não estão presentes na sequência esquerda).

Essas propriedades são importantes, porque são o que alguém novo no FullOuterJoin, mas com experiência no LINQ, espera.

Søren Boisen
fonte
Ele não preserva a ordem das seqüências de entrada: a pesquisa não garante isso; portanto, esses foreaches serão enumerados em alguma ordem do lado esquerdo e, em seguida, alguma ordem do lado direito não presente no lado esquerdo. Mas a ordem relacional dos elementos não é preservada.
Ivan Danilov
@IvanDanilov Você está certo de que isso não está realmente no contrato. A implementação do ToLookup, no entanto, usa uma classe Lookup interna no Enumerable.cs que mantém agrupamentos em uma lista vinculada de inserção ordenada e usa essa lista para iterá-los. Portanto, na versão atual do .NET, o pedido é garantido, mas, como a Microsoft infelizmente não documentou isso, eles podem alterá-lo em versões posteriores.
Søren Boisen
Eu tentei no .NET 4.5.1 no Win 8.1 e ele não preserva a ordem.
Ivan Danilov
1
"..as seqüências de entrada são ansiosamente enumeradas pelas chamadas para o ToLookup". Mas sua implementação faz exatamente o mesmo. O rendimento não gera muito aqui por causa das despesas em máquinas de estado finito.
Pkuderov
4
As chamadas de pesquisa são feitas quando o primeiro elemento do resultado é solicitado, e não quando o iterador é criado. É isso que significa execução adiada. Você pode adiar ainda mais a enumeração de um conjunto de entradas, iterando diretamente o Enumerable esquerdo em vez de convertê-lo em uma Pesquisa, resultando no benefício extra de que a ordem do conjunto esquerdo é preservada.
Rolf
2

Decidi adicionar isso como uma resposta separada, pois não tenho certeza de que foi testado o suficiente. Esta é uma reimplementação do FullOuterJoinmétodo usando essencialmente uma versão simplificada e personalizada de LINQKit Invoke/ Expandfor, Expressionpara que ele funcione no Entity Framework. Não há muita explicação, pois é praticamente a mesma que a minha resposta anterior.

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
fonte
NetMage, codificação impressionante! Quando eu o executo com um exemplo simples, e quando o [NullVisitor.Visit (..) é chamado em [base.Visit (Node)], ele gera uma [System.ArgumentException: Argument Types não corresponde]. O que é verdade, pois estou usando uma TKey [Guid] e, em algum momento, o visitante nulo espera um tipo [Guid?]. Pode estar faltando alguma coisa. Eu tenho um pequeno exemplo codificado para EF 6.4.4. Informe-me como posso compartilhar esse código com você. Obrigado!
Troncho 16/06
@Troncho Normalmente, uso o LINQPad para testes, portanto o EF 6 não é fácil. base.Visit(node)não deve lançar uma exceção, pois isso apenas se repete na árvore. Eu posso acessar praticamente qualquer serviço de compartilhamento de código, mas não configurar um banco de dados de teste. Porém, executá-lo no meu teste LINQ to SQL parece funcionar bem.
NetMage 16/06
@Troncho É possível que você esteja se unindo entre uma Guidchave e uma Guid?chave estrangeira?
NetMage 16/06
Também estou usando o LinqPad para testar. Minha consulta lançou o ArgumentException, por isso decidi depurá-lo no VS2019 no [.Net Framework 4.7.1] e no EF 6. mais recente. Lá, consegui rastrear o problema real. Para testar seu código, estou gerando 2 conjuntos de dados separados, originários da mesma tabela [Pessoas]. Eu filtro os dois conjuntos para que alguns registros sejam exclusivos de cada conjunto e alguns existam nos dois conjuntos. [PersonId] é um guia [Chave Primária] (c #) / Identificador exclusivo (SqlServer) e nenhum conjunto gera nenhum valor [PersonId] nulo. Código compartilhado: github.com/Troncho/EF_FullOuterJoin
Troncho
1

Executa uma enumeração de streaming na memória em ambas as entradas e chama o seletor para cada linha. Se não houver correlação na iteração atual, um dos argumentos do seletor será nulo .

Exemplo:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Requer um IComparer para o tipo de correlação, usa o Comparer.Default se não for fornecido.

  • Requer que 'OrderBy' seja aplicado aos enumeráveis ​​de entrada

    /// <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;
                }
            }
    
        }
    
    }
James Caradoc-Davies
fonte
1
Esse é um esforço heróico para fazer as coisas "fluírem". Infelizmente, todo o ganho é perdido na primeira etapa, onde você executa as OrderByduas principais projeções. OrderByarmazena em buffer a sequência inteira, pelas razões óbvias .
sehe
@sehe Você está definitivamente correto para o Linq to Objects. Se o IEnumerable <T> for IQueryable <T>, a fonte deve classificar - não há tempo para testar. Se eu estiver errado, basta substituir a entrada IEnumerable <T> por IQueryable <T>, que deve ser classificada na origem / banco de dados.
James Caradoc-Davies
1

Minha solução limpa para a situação em que a chave é única nos dois enumeráveis:

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

tão

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

saídas:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi
Guido Mocha
fonte
0

Junção externa completa para duas ou mais tabelas: Primeiro extraia a coluna na qual deseja ingressar.

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

Em seguida, use a junção externa esquerda entre a coluna extraída e as tabelas principais.

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();
Mohammad_Iranian
fonte
0

Eu escrevi essa classe de extensões para um aplicativo há talvez 6 anos e uso-a desde então em muitas soluções sem problemas. Espero que ajude.

editar: notei que alguns podem não saber como usar uma classe de extensão.

Para usar esta classe de extensão, basta referenciar seu espaço para nome na sua classe, adicionando a seguinte linha usando joinext;

^ isso deve permitir que você veja o sentido das funções de extensão em qualquer coleção de objetos IEnumerable que você usar.

Espero que isto ajude. Deixe-me saber se ainda não está claro, e espero escrever um exemplo de exemplo de como usá-lo.

Agora aqui está a 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)
                        ));

        }

    }
}
H7O
fonte
1
Infelizmente, parece que a função in SelectManynão pode ser convertida em uma árvore de expressão digna de LINQ2SQL, ao que parece.
OR Mapper
edc65. Eu sei que pode ser uma pergunta boba, se você já fez isso. Mas por precaução (como notei que alguns não sabem), basta fazer referência ao namespace joinext.
H7O 03/10/19
OU Mapper, deixe-me saber com que tipo de coleção você deseja que ela funcione. Ele deve funcionar bem com qualquer coleção IEnumerable
H7O
0

Eu acho que a cláusula de junção LINQ não é a solução correta para esse problema, porque o objetivo da cláusula de junção não é acumular dados da maneira necessária para esta solução de tarefa. O código para mesclar coleções separadas criadas se torna muito complicado, talvez seja bom para fins de aprendizado, mas não para aplicativos reais. Uma das maneiras de resolver esse problema está no código abaixo:

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 coleções reais são grandes para a formação do HashSet, em vez disso, cada loop pode ser usado no código abaixo:

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
Sharunas Bielskis
fonte
0

Obrigado a todos pelas postagens interessantes!

Eu modifiquei o código porque no meu caso eu precisava

  • um predicado de junção personalizado
  • um comparador distinto de união personalizada

Para os interessados, este é o meu código modificado (em VB, desculpe)

    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
Alberto Orlandini
fonte
0

Ainda outra junção externa completa

Como não estava tão feliz com a simplicidade e a legibilidade das outras proposições, acabei com isso:

Ele não tem a pretensão de ser rápido (cerca de 800 ms para ingressar em 1000 * 1000 em uma CPU de 2020m: 2,4ghz / 2cores). Para mim, é apenas uma junção externa completa compacta e casual.

Funciona da mesma forma que uma junção externa completa do SQL (conservação duplicada)

Felicidades ;-)

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

A ideia é

  1. Crie IDs com base nos principais construtores de função fornecidos
  2. Processar apenas itens restantes
  3. Junção interna do processo
  4. Processar apenas itens corretos

Aqui está um teste sucinto que o acompanha:

Coloque um ponto de interrupção no final para verificar manualmente se ele se comporta conforme o esperado

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

}

Julien R
fonte
-4

Eu realmente odeio essas expressões linq, é por isso que o SQL existe:

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

Crie isso como sql view no banco de dados e importe-o como entidade.

É claro que a união (distinta) de junções esquerda e direita também será suficiente, mas é estúpido.

Milan Švec
fonte
11
Por que não simplesmente largar o máximo de abstrações possível e fazer isso no código da máquina? (Dica: porque as abstrações de ordem superior facilitam a vida do programador). Isso não responde à pergunta e me parece mais um discurso retórico contra o LINQ.
gastador
8
Quem disse que os dados vêm de um banco de dados?
user247702
1
Obviamente, é banco de dados, existem as palavras "junção externa" em questão :) google.cz/search?q=outer+join
Milan Švec
1
Entendo que esta é uma solução "antiquada", mas antes da votação, compare sua complexidade com outras soluções :) Exceto a aceita, é claro que é a correta.
Milan Švec
Claro que pode ser um banco de dados ou não. Estou à procura de uma solução com uma associação externa entre listas na memória
edc65