1

I have a class Project as

public class Project 
{   public int ProjectId { get; set; }
    public string ProjectName { get; set; }
    public string Customer { get; set; }
    public string Address{ get; set; }
}

and I have 3 lists

List<Project> lst1; List<Project> lst2; List<Project> lst3;

lst1 contains Person objects with ProjectId and ProjectName.

ProjectId =1, ProjectName = "X", Customer = null, Address = null

ProjectId =2, ProjectName = "Y", Customer = null, Address = null

lst2 contains Person objects with ProjectId and Customer

ProjectId =1,ProjectName = null, Customer = "c1", Address = null

ProjectId =2,ProjectName = null, Customer = "c2", Address = null , and

lst3 contains Person objects with ProjectId and Address

ProjectId = 1, ProjectName = null, Customer =null, Address = "a1"

ProjectId = 2, ProjectName = null, Customer =null, Address = "a2".

Considering there are multiple such records in each list and ProjectId is Uniqe for each project, How can I merge/combine these list to get one list with merged objects

ProjectId=1, ProjectName="X", Customer="c1", address="a1"

ProjectId=2, ProjectName="Y", Customer="c2", address="a2"

I found thse links similar and tried with it but could not meet the results

Create a list from two object lists with linq

How to merge two lists using LINQ?

Thank You.

Community
  • 1
  • 1
quitprog
  • 491
  • 1
  • 9
  • 24

6 Answers6

4

This could be done in a multi-step approach pretty simply. First, define a Func<Project, Project, Project> to handle the actual record merging. That is, you are defining a method with a signature equivalent to public Project SomeMethod(Project p1, Project p2). This method implements the merging logic you outlined above. Next, we concatenate the elements of the lists together before grouping them by ProjectId, using our merge delegate as the an aggregate function in the overload of GroupBy which accepts a result selector:

Func<Project, Project, Project> mergeFunc = (p1,p2) => new Project
    {
        ProjectId = p1.ProjectId,
        ProjectName = p1.ProjectName == null ? p2.ProjectName : p1.ProjectName,
        Customer = p1.Customer == null ? p2.Customer : p1.Customer,
        Address = p1.Address == null ? p2.Address : p1.Address    
    };

var output = lst1.Concat(lst2).Concat(lst3)
                 .GroupBy(x => x.ProjectId, (k, g) => g.Aggregate(mergeFunc)); 

Here's a quick and dirty test of the above logic along with output:

List<Project> lst1; List<Project> lst2; List<Project> lst3;
lst1 = new List<Project> 
    {
        new Project { ProjectId = 1, ProjectName = "P1" },
        new Project { ProjectId = 2, ProjectName = "P2" },
        new Project { ProjectId = 3, ProjectName = "P3" }
    };
lst2 = new List<Project>
    {
        new Project { ProjectId = 1, Customer = "Cust1"},
        new Project { ProjectId = 2, Customer = "Cust2"},
        new Project { ProjectId = 3, Customer = "Cust3"}
    };
lst3 = new List<Project>
    {
        new Project { ProjectId = 1, Address = "Add1"},
        new Project { ProjectId = 2, Address = "Add2"},
        new Project { ProjectId = 3, Address = "Add3"}
    };

Func<Project, Project, Project> mergeFunc = (p1,p2) => new Project
    {
        ProjectId = p1.ProjectId,
        ProjectName = p1.ProjectName == null ? p2.ProjectName : p1.ProjectName,
        Customer = p1.Customer == null ? p2.Customer : p1.Customer,
        Address = p1.Address == null ? p2.Address : p1.Address    
    };

var output = lst1
    .Concat(lst2)
    .Concat(lst3)
    .GroupBy(x => x.ProjectId, (k, g) => g.Aggregate(mergeFunc));

IEnumerable<bool> assertedCollection = output.Select((x, i) => 
    x.ProjectId == (i + 1) 
    && x.ProjectName == "P" + (i+1) 
    && x.Customer == "Cust" + (i+1) 
    && x.Address == "Add" + (i+1));

Debug.Assert(output.Count() == 3);  
Debug.Assert(assertedCollection.All(x => x == true));

--- output ---

IEnumerable<Project> (3 items)   
ProjectId ProjectName Customer Address 
1 P1 Cust1 Add1 
2 P2 Cust2 Add2 
3 P3 Cust3 Add3 
Josh E
  • 7,390
  • 2
  • 32
  • 44
2

Using a Lookup you can do it like this:

        List<Project> lst = lst1.Union(lst2).Union(lst3).ToLookup(x => x.ProjectId).Select(x => new Project()
        {
            ProjectId = x.Key,
            ProjectName = x.Select(y => y.ProjectName).Aggregate((z1,z2) => z1 ?? z2),
            Customer = x.Select(y => y.Customer).Aggregate((z1, z2) => z1 ?? z2),
            Address = x.Select(y => y.Address).Aggregate((z1, z2) => z1 ?? z2)
        }).ToList();
cdel
  • 717
  • 7
  • 14
  • Works perfectly. Thank You. I think I need to search how ToLookup works. – quitprog May 29 '12 at 13:03
  • although this solution works, you don't need to project the properties prior to aggregation. See my answer for how you can do the same thing in a bit more of a readable fashion. The lookup is also optional, and is roughly equivalent to the GroupBy in this context. – Josh E May 29 '12 at 14:36
1

I assume that list contains same number of items and are sorted by ProjectId.

List<Project> lst1; List<Project> lst2; List<Project> lst3

If list are not sorted you can sort it first.

list1.Sort(p => p.ProjectId);
list2.Sort(p => p.ProjectId);
list3.Sort(p => p.ProjectId);

For merging the object

List<Project> list4 = new List<Project>();
for(int i=1; i<list.Count; i++)
{ 
    list4.Add(new Project
    {
       ProjectId = list1[i].ProjectId;
       ProjectName = list1[i].ProjectName;
       Customer = list2[i].Customer;
       Address = list3[i].Address;
    });

}
Asif Mushtaq
  • 13,010
  • 3
  • 33
  • 42
1

I belive the folloing is how LINQ Join works:

var mergedProjects =
    lst1
        .Join(lst2,
            proj1 => proj1.ProjectID,
            proj2 => proj2.ProjectID,
            (proj1, proj2) => new { Proj1 = proj1, Proj2 = proj2 })
        .Join(lst3,
            pair => pair.Proj1.ProjectID,
            proj3 => proj3.ProjectID,
            (pair, proj3) => new Project
            {
                ProjectID = proj3.ProjectID,
                ProjectName = pair.Proj1.ProjectName,
                Customer = pair.Proj2.Customer,
                Address = proj3.Address
            });

This will not return any results where the ProjectID is not found in all three lists.

If this is a problem, I think you'd be better off doing this manually rather than using LINQ.

Rawling
  • 49,248
  • 7
  • 89
  • 127
  • although this addresses the OP question, it does so pretty narrowly. This solution would seem a bit brittle. It would be better, IMO, if the code made the merging logic explicit in the operation instead of assuming that `projXX.PropXX` maps in a particular way (no downvote, BTW, just an observation) – Josh E May 29 '12 at 15:21
  • @Josh The question was pretty specific about each property coming from a given list, less so about all IDs being present in each list. It would have been clearer to collate all the items with a given ID into a single anonymous object and then gather the properties as a final step, rather than gather the properties one-by-one. This isn't very extensible if you have other lists with other properties, or more than one property per list, but these weren't stated in the original question. – Rawling May 30 '12 at 07:16
  • there's always 1...N ways to skin a cat, right? Fair points you make. – Josh E May 30 '12 at 16:16
1

Although overkill, I was tempted to make this an extension method:

public static List<T> MergeWith<T,TKey>(this List<T> list, List<T> other, Func<T,TKey> keySelector, Func<T,T,T> merge)
{
    var newList = new List<T>();
    foreach(var item in list)
    {
        var otherItem = other.SingleOrDefault((i) => keySelector(i).Equals(keySelector(item)));
        if(otherItem != null)
        {
            newList.Add(merge(item,otherItem));
        }
    }
    return newList;
}

Usage would then be:

var merged = list1
     .MergeWith(list2, i => i.ProjectId,
       (lhs,rhs) => new Project{ProjectId=lhs.ProjectId,ProjectName=lhs.ProjectName, Customer=rhs.Customer})
    .MergeWith(list3,i => i.ProjectId,
       (lhs,rhs) => new Project{ProjectId=lhs.ProjectId,ProjectName=lhs.ProjectName, Customer=lhs.Customer,Address=rhs.Address});

Live example: http://rextester.com/ETIVB14254

Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • This works perfectly for me. Thank You. Just one thing, why overkill ? I mean in terms of performance or what ? – quitprog May 29 '12 at 12:49
  • It's overkill because it is a bit of a complex solution to a relatively simple problem (@Jamiec - that what you meant?). Also, the manner of which the merging is accomplished will result/could result in multiple executions of the same query expression. Finally, there are other operators (namely `.Zip()`) that duplicate some (but not all) this functionality. – Josh E May 29 '12 at 16:13
  • 1
    @JoshE - pretty much exactly. I wrote this rather quick, and just knew there would be a better way. – Jamiec May 30 '12 at 07:51
0

This is assuming that you want to take the first non-null value, or revert to the default value - in this case null for a string.

private static IEnumerable<Project> GetMergedProjects(IEnumerable<List<Project>> projects)
{
    var projectGrouping = projects.SelectMany(p => p).GroupBy(p => p.ProjectId);
    foreach (var projectGroup in projectGrouping)
    {
        yield return new Project
                            {
                                ProjectId = projectGroup.Key,
                                ProjectName =
                                    projectGroup.Select(p => p.ProjectName).FirstOrDefault(
                                        p => !string.IsNullOrEmpty(p)),
                                Customer =
                                    projectGroup.Select(c => c.Customer).FirstOrDefault(
                                        c => !string.IsNullOrEmpty(c)),
                                Address =
                                    projectGroup.Select(a => a.Address).FirstOrDefault(
                                        a => !string.IsNullOrEmpty(a)),
                            };
    }
}

You could also make this an extension method if needed.

Mike
  • 6,149
  • 5
  • 34
  • 45