Let's do some fancy set operations using the new UnionBy(), IntersectBy(), ExceptBy(), and DistinctBy() methods in .NET 6!

A single chair sits in a fully-lit recording set, complete with lights and microphones.
Wrong kind of set, but I appreciate the effort. Photo by Keagan Henman / Unsplash

Current Implementation

.NET 5 and below include operations in LINQ that are collectively called set operations; that is, they operate on sets of values compared to other sets of values.

For example, the Union() method produces the set union by combining all values from two sets, while removing the duplicates.

var numbers1 = new List<int>() {2, 5, 6, 9};
var numbers2 = new List<int>() {5, 8, 10};

var union = numbers1.Union(numbers2);
//2, 5, 6, 9, 8, 10

The Intersect() method produces the values that appear in both the sets.

var numbers1 = new List<int>() {2, 5, 6, 9};
var numbers2 = new List<int>() {5, 8, 10};

var intersect = numbers1.Intersect(numbers2);
//5

The Except() method gives the numbers that only appear in the first set, and do not appear in the second set.

var numbers1 = new List<int>() {2, 5, 6, 9};
var numbers2 = new List<int>() {5, 8, 10};

var except = numbers1.Except(numbers2);
//2, 6, 9

Finally, the Distinct() method operates on a single set and gives the distinct values from within that set.

var numbers = new List<int>() {6, 1, 6, 7, 1, 8, 1, 2, 3, 2};

var distinct = numbers.Distinct();
//7, 8, 3

Just like we saw in the previous post about MaxBy() and MinBy(), these kinds of operations are great if you are only working with primitive types. But what if you need to work with complex collections of classes? .NET 6 includes a new set of methods in the LINQ library to help with those situations.

New Implementation

Let's say we have the User class from the previous post, and two collections of users.

public class User
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
}

public class LINQSetByExamples
{
    private List<User> GetUserList()
    {
        return new List<User>()
        {
            new User()
            {
                ID = 1,
                FirstName = "Terrance",
                LastName = "Johnson",
                DateOfBirth = new DateTime(1985, 12, 6)
            },
            new User()
            {
                ID = 2,
                FirstName = "Angelica",
                LastName = "Johnson",
                DateOfBirth = new DateTime(1984, 8, 22)
            },
            new User()
            {
                ID = 3,
                FirstName = "Jackson",
                LastName = "Browne",
                DateOfBirth = new DateTime(2001, 7, 29)
            },
            new User()
            {
                ID = 4,
                FirstName = "Hailee",
                LastName = "Escobar",
                DateOfBirth = new DateTime(2004, 1, 16)
            }
        };
    }

    private List<User> GetUserList2()
    {
        return new List<User>()
        {
            new User()
            {
                ID = 1,
                FirstName = "Jeremy",
                LastName = "Yardling",
                DateOfBirth = new DateTime(2001, 6, 1)
            },
            new User()
            {
                ID = 2,
                FirstName = "Toni",
                LastName = "Berkowitz",
                DateOfBirth = new DateTime(2004, 1, 16)
            },
            new User()
            {
                ID = 3,
                FirstName = "Vanessa",
                LastName = "Warren",
                DateOfBirth = new DateTime(1975, 10, 11)
            },
            new User()
            {
                ID = 4,
                FirstName = "Lawrence",
                LastName = "Neilson",
                DateOfBirth = new DateTime(1966, 9, 30)
            }
        };
    }
}
Our Examples() method will exist inside the LINQSetByExamples class.

UnionBy

We can use the new UnionBy() method to do a complex calculation: get users from both sets who have distinct birth years. This calculation "favors" the first set, because if users in the second set have the same birth year as users in the first set, the users from the first set will be included and the users from the second set will not.

public void Examples()
{
    var users = GetUserList();
    var users2 = GetUserList2();

    var unionBirthYear = users.UnionBy(users2, x => x.DateOfBirth.Year);
    //Terrance Johnson
    //Angelica Johnson
    //Jackson Browne
    //Hailee Escobar
    //Vanessa Warren
    //Lawrence Neilson
}
Excluded Jeremy Yardling and Toni Berkowitz

IntersectBy

The IntersectBy() method can be used to find users who do not share a birth year with anyone from the opposite set.

public void Examples()
{
    var users = GetUserList();
    var users2 = GetUserList2();

    var intersectionBirthYear 
        = users.IntersectBy(users2.Select(x => x.DateOfBirth.Year),
                            x => x.DateOfBirth.Year);
    //Terrance Johnson
    //Angelica Johnson
    //Vanessa Warren
    //Lawrence Neilson
}
Four users were excluded because they share birth years.

ExceptBy

What if we wanted to find all users in both sets, except those who have particular first names? We can do that using ExceptBy().

public void Examples()
{
    List<User> users = GetUserList();
    var users2 = GetUserList2();
    users.AddRange(users2);
    
    var names = new List<string>() { "Hailee", "Jackson", "Jeremy" };
    var exceptByFirstName = users.ExceptBy(names, x => x.FirstName);
    
    //Terrance Johnson
    //Angelica Johnson
    //Toni Berkowitz
    //Vanessa Warren
    //Lawrence Neilson
}

DistinctBy

Finally, we can do things like "get all users by distinct birth dates" using DistinctBy():

public void Examples()
{
    List<User> users = GetUserList();
    var users2 = GetUserList2();
    users.AddRange(users2);
    
    var usersWithDistinctBirthDates = users.DistinctBy(x => x.DateOfBirth);
    //Terrance Johnson
    //Angelica Johnson
    //Jackson Browne
    //Hailee Escobar
    //Jeremy Yardling
    //Vanessa Warren
    //Lawrence Neilson
}
Toni Berkowitz is excluded because she has the same birth date as Hailee Escobar

Other Considerations

With these methods, order of invocation matters. For example, consider the following code, which is nearly the same as the earlier use of IntersectBy() with two small changes.

public void Examples()
{
    var users = GetUserList();
    var users2 = GetUserList2();

    var intersectionBirthYear 
        = users2.IntersectBy(users.Select(x => x.DateOfBirth.Year),
                             x => x.DateOfBirth.Year);
    //Jeremy Yardling
    //Toni Berkowitz
    //Vanessa Warren
    //Lawrence Neilson
}

Because users2 is now the "primary" set, we get a completely different result set than in the earlier example. Be careful with your order of invocation!

Demo Project

As with all posts in this series, there is a demo project you can check out over on GitHub with the examples for UnionBy(), IntersectBy(), DistinctBy(), and ExceptBy() found in this post. Check it out!

GitHub - exceptionnotfound/NET6BiteSizeDemos
Contribute to exceptionnotfound/NET6BiteSizeDemos development by creating an account on GitHub.

Happy Coding!