Home > Mobile >  LINQ Union two lists that have same ID but different types
LINQ Union two lists that have same ID but different types

Time:12-08

I am trying to merge two lists into one List without duplicates

JOIN operator returns only common elements

These are lists in JSON

List1 is:

{
    "screenID": 96,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
},
{
    "screenID": 97,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
},
{
    "screenID": 98,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
}

List2 is:

{
    "screenID": 96,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": false
},
{
    "screenID": 97,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": false
}

If ScreenID is same then I want to compare between CRUD elements like:

if(ScreenID == 96){
Create = List1.Create == true && List2.Create == false ? true : false
}

I tried this : var finalList = list1.Union(list2);

but the result was:

{
    "screenID": 96,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
},
{
    "screenID": 97,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
},
{
    "screenID": 98,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": true
},
{
    "screenID": 96,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,
    "print": false
},
{
    "screenID": 97,
    "create": true,
    "read": true,
    "update": true,
    "delete": true,

I am beginner in LINQ so any help is appreciated

EDIT I am using .NET 3.1

CodePudding user response:

EDIT: I've made some changes to adapt my original answer to .Net 3.5, this should now work provided you're working with C# 7.3 or later (Set by <LangVersion>7.3</LangVersion> in your .csproj file). I ditched the HashSet<T> for a Dictionary<T> which actually made the whole thing less verbose.

While the exising answer is good I'd like to add an alternate approach using an extension method that essentially should do the same as UnionBy but accept an additonal Func<T, T, T> for handling duplicates.

The UnionComparer makes it a bit verbose but that essentially just wraps the IEqualityComparer<TKey> for the key selector.

public static IEnumerable<T> UnionBy<T, TKey>(
    this IEnumerable<T> first,
    IEnumerable<T> second,
    Func<T, TKey> keySelector,
    Func<T, T, T> duplicateHandler,
    IEqualityComparer<TKey> keyComparer = null)
{
    if(keyComparer is null)
        keyComparer = EqualityComparer<TKey>.Default;

    var result = new Dictionary<TKey, T>(keyComparer);

    foreach (var item in first)
        result.Add(keySelector(item), item);

    foreach (var item in second)
    {
        var key = keySelector(item);
        if (result.TryGetValue(key, out var duplicate))
        {
            result[key] = duplicateHandler(item, duplicate);
        }
        else
        {
            result.Add(key, item);
        }
    }
    return result.Values;
}

I've made a simple example class for your data:

class Operation
{
    public int ScreenId { get; set; }
    public bool Create { get; set; }
}

Usage:

var first = new List<Operation>()
{
    new Operation () { ScreenId = 1, Create = false },
    new Operation () { ScreenId = 2, Create = false },
    new Operation () { ScreenId = 3, Create = false },
};
            var second = new List<Operation>()
{
    new Operation () { ScreenId = 3, Create = true },
    new Operation () { ScreenId = 4, Create = true },
    new Operation () { ScreenId = 5, Create = true },
};

var result = first.UnionBy(
    second,
    o => o.ScreenId,
    (a, b) => new Operation { ScreenId = a.ScreenId, Create = a.Create || b.Create });

foreach (var operation in result)
    Console.WriteLine($"ID: {operation.ScreenId}, Create: {operation.Create}");

Console.ReadLine();

Result

EDIT: I just saw you are limited to .Net 3.5, I'm not entirely sure you will not have to make any changes to this but HashSet<T> and IEqualityComparer<T> should be supported

CodePudding user response:

.NET 6 introduced UnionBy which can be used to combine two collections based on an expression. When duplicates are found, the method returns the first item it encounters. The two collections should be ordered first to control which item is returned :

var ordA=colA.OrderByDescending(x=>new {x.ScreenId,x.Create,x.Update,x.Delete});
var ordB=colB.OrderByDescending(x=>new {x.ScreenId,x.Create,x.Update,x.Delete});

var unioned=ordA.UnionBy(ordB,x=>x.ScreenId);

For more complex logic, the order expression should be modified to ensure the desired item comes first. For example, to select the item with the most flags, Enumerable.Count(t=>t) can be used :

var ordA=colA.OrderByDescending(x=>new {
    x.ScreenId,
    Count=new[]{ x.Create,x.Update,x.Delete }
              .Count(t=>t)
});

Using DistinctBy

In previous versions .NET versions the MoreLINQ library can be used. There's no UnionBy but DistinctBy can be used for the same job after concatenation. Once again, the order controls what is returned :

var all=colA.Concat(colB);

var unioned=all.OrderByDescending(x=>new { x.ScreenId,x.Create,x.Update,x.Delete})
               .DistinctBy(x=>x.ScreenId);

DistinctBy is also available in .NET 6.

  • Related