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.

3
  • In each list, is there always (besides projectId) exactly one property not null? Commented May 29, 2012 at 12:15
  • And is there a fixed set of properties, known in advance? Commented May 29, 2012 at 12:17
  • @phg : No. to both questions. This is just example. And also lists may not contain same number of records. Commented May 29, 2012 at 12:24

6 Answers 6

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 
Sign up to request clarification or add additional context in comments.

Comments

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

2 Comments

Works perfectly. Thank You. I think I need to search how ToLookup works.
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.
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.

3 Comments

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 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.
there's always 1...N ways to skin a cat, right? Fair points you make.
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;
    });

}

4 Comments

This is a nice simple way of doing things if the items are all in the right order.
lists may not contain same number of records. And even if they have same records ProjectId may differ.
Then on which criteria you will merge them?
@Asif : Every list will have some projects with ProjectId, So for each projectId I want to gather data from different lists and make a single list.
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

3 Comments

This works perfectly for me. Thank You. Just one thing, why overkill ? I mean in terms of performance or what ?
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.
@JoshE - pretty much exactly. I wrote this rather quick, and just knew there would be a better way.
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.