Search code examples
c#linqleft-joindefaultifempty

C# Linq DefaultIfEmpty LeftJoin


Some Agents has null in Group field. I'm trying to do LeftJoin, but receive result like InnerJoin (only Agents with not null Group)

Agents = new ObservableCollection<dynamic>((await _repository.GetAgentsAsync() ?? new Agent[] { })
            .Join(Groups.DefaultIfEmpty(), a => a.Group, g => g.ID, (a, g) =>
            new { ID = a.ID, AgentName = a.AgentName, Login = a.Login, AgentID = a.AgentID, IsDel = a.IsDel, Group = g == null ? "Empty" : $"{g.NameGroup} ({g.Num})" }));

what the trouble is?

Thanks all, I found answer https://stackoverflow.com/a/21584913/13618303

        Groups = new ObservableCollection<Group>(await _repository.GetGroupsAsync() ?? new Group[] { });
        Agents = new ObservableCollection<Agent>(await _repository.GetAgentsAsync() ?? new Agent[] { });
        AgentsGroups = new ObservableCollection<dynamic>(Agents.GroupJoin(Groups, a => a.Group, g => g.ID, (a, g) => new { Agent = a, Group = g})
            .SelectMany(ag => ag.Group.DefaultIfEmpty(), (a,g) => new { Agent = a.Agent, Group = g })
            .Select ( ag => new { ID = ag.Agent.ID, AgentName = ag.Agent.AgentName, Login = ag.Agent.Login, AgentID = ag.Agent.AgentID, IsDel = ag.Agent.IsDel, Group = ag.Group == null ? "Empty" : $"{ag.Group.NameGroup} ({ag.Group.Num})" }));

Solution

  • Method syntax doesn't use Join to perform a left join style link, it uses GroupJoin. If you straight Join then the lack of an element on the right (Group) will mean that the element on the left (Agent) disappears.

    A GroupJoin pairs elements on the left with multiple matching elements on the right

    Left
    1, John
    2, Mary
    3, Joe
    
    Right
    1, The Street, 1996
    1, The Avenue, 2002
    2, The Road, 2010
    

    John lived in two places, mary lived in one. Joe never lived anywhere. Left.GroupJoin(Right) produces:

    {1, John}, [ {1, The Street, 1996}, {1, The Avenue, 2002} ]
    {2, Mary}, [ {1, The Road, 2010} ]
    {3, Joe},  [ ]
    

    GroupJoin will keep the element on the left and make the element on the right an empty sequence if there is no match

    If you needed this back out as a list with repeating elements, you'd SelectMany it, which expands the list-of-lists that the person:addresses represents. I'll come back to this.


    DefaultIfEmpty does not enable left join behavior; it is just a convenience that returns a sequence containing one item of the target type (and with the default value for the type), if the sequence it is called on has zero elements. In essence it's like:

    public int[] DefaultIfEmpty(int[] input){
      if(input.Length == 0)
        return new int[1]; //a single integer, value 0 --> the default for int
      else
        return input;
    }
    

    You don't need it on Groups; it'd be a non op anyway, because if there are 0 Groups, converting it to a list of a single null Group will behave the same (no agent will match)

    You do want DefaultIfEmpty if you plan on using SelectMany on your grouping, because SelectMany will not operate at all on a collection of 0 elements. Because Joe had no addresses, SelectMany would skip over him and he would be lost from the output


    All in, this means you should execute a GroupJoin, and you don't really need a DefaultIfEmpty anywhere because you aren't doing a SelectMany/you can tolerate there being 0 Groups for an Agent in other ways.

    I'll use more full names, because it's unfortunate that you're joining to a class called Group (I guess), and a groupjoin gives you a list of Agents, and each Agent in the list has a list of matching Group:

    .GroupJoin(Groups, agent => agent.Group, group => group.ID, (agent, groupCollection) =>
        new {  
            agent.ID, 
            agent.AgentName,
            agent.Login,
            agent.AgentID, 
            agent.IsDel, 
            GroupNameAndNum = groupCollection.Select(g => $"{g.NameGroup} ({g.Num})").FirstOrDefault() ?? "Empty" 
        }
    );
    

    If you were using SelectMany (perhaps you have agents that have 2 groups and want them listed separately, with the agent data repeated), it would perhaps look like:

    .GroupJoin(Groups, agent => agent.Group, group => group.ID, (agent, groupCollection) => new { Agent = agent, OneOrMoreGroups = groupCollection.DefaultIfEmpty() })
    .SelectMany(
        agentWithGroupsCollection => agentWithGroupsCollection.OneOrMoreGroups,
        (agentWithGroupsCollection, groupsCollectionItem) =>
    
        new {  
            agentWithGroupsCollection.ID, 
            agentWithGroupsCollection.AgentName,
            agentWithGroupsCollection.Login,
            agentWithGroupsCollection.AgentID, 
            agentWithGroupsCollection.IsDel, 
            GroupNameAndNum = groupsCollectionItem == null ? "Empty" : $"{groupsCollectionItem.NameGroup} ({groupsCollectionItem.Num})"
        }
    );