Lists in C# are one of the most ubiquitous data structures. We perform all kinds of operations on them, such as appending, inserting, removing, sorting, and filtering. In this article, we’ll focus on the latter – filtering. In particular, we’ll see how to use a list to filter other lists. We can achieve it in several ways, with or without LINQ.

To download the source code for this article, you can visit our GitHub repository.

Let’s have a look at the most typical scenarios. 

Filtering a List

Filtering a list generally means keeping some elements in the list, while removing others. We have to pick a criterion on which this selection is based.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

So, we have two lists, one for filtering and one the filtering is based on. With that in mind, let’s look at a high-level view of filtering possibilities: 

  • Select or remove elements from the List 1 that are also present in the List 2
  • Select or remove elements from the List 1 that are not present in the List 2
  • Select or remove elements from List 1 based on the elements in List 2 (e.g. List 2 contains indices of elements to remove)
  • Use List 2 as a mask (for example, boolean values that determine which elements from List 1 to preserve)

Before we proceed, let’s create two lists for use in our first examples:

List<int> intsToFilter = [1, 2, 3, 4, 4, 4, 5, 6, 7, 7, 8, 8, 9];
List<int> filteringIntList = [4, 4, 7, 9, 10];

Let’s start with loops.

Using Loops to Filter a List

One of the first ways in which we can filter lists is via simple loops (like for and foreach). Suppose we want the first list to contain only the elements that are also in the second list. Let’s create a method FilterContainedUsingLoop to handle it:

public static List<int> FilterContainedUsingLoop(List<int> listToFilter, List<int> filteringList)
{
    List<int> filteredList = [];

    foreach (int item in listToFilter)
    {
        if (filteringList.Contains(item))
            filteredList.Add(item);
    }

    return filteredList;
}

Here we iterate through our listToFilter, adding items to our resulting filteredList as long as the item is contained within our filteringList.

Exercising the method with our previously defined lists results in:

4, 4, 4, 7, 7, 9

Here we retain only the elements from the second list. We could also filter the first list by the elements not contained in the second list:

public static List<int> FilterNotContainedUsingLoop(List<int> listToFilter, List<int> filteringList)
{
    List<int> filteredList = [];

    foreach (int item in listToFilter)
    {
        if (!filteringList.Contains(item))
            filteredList.Add(item);
    }

    return filteredList;
}

This code is similar to the previous method with the exception that now we add items to our resultant filteredList when they are not contained in the filteringList:

1, 2, 3, 5, 6, 8, 8

Filtering without LINQ is possible, as we can see. But LINQ opens up a whole plethora of other possibilities for us.

Filter a List Using the Where Extension Method

We can simplify the examples above by using the Where() extension method. Let’s first filter List 1 by the elements contained in List 2:

public static List<int> FilterContainedUsingWhere(List<int> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(filteringList.Contains).ToList();
}

Here, the Where() method will call filteringList.Contains() on each element of listToFilter, just as we saw in our foreach loop example. Where() returns an IEnumerable<T>, and so to get our final result, we call ToList() to return a new list containing only the elements in listToFilter that were also contained in filteringList:

4, 4, 4, 7, 7, 9

Similarly, we can use the Where() method to filter for elements that are not contained in the second list:

public static List<int> FilterNotContainedUsingWhere(List<int> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(x => !filteringList.Contains(x)).ToList();
}

The filtered list now looks like before:

1, 2, 3, 5, 6, 8, 8

In both examples, we explicitly filter elements that are either in or not in the filtering list. But what if we wanted to have just unique values, i.e. not only filter the list, but also remove duplicates? Well, this is pretty easy to do.

Removing Duplicates

The methods we’ve seen thus far retain all filtered values in the first list, even if they are not unique. If we want to remove duplicates, we can add a call to the Distinct() method:

public static List<int> FilterContainedUnique(List<int> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(filteringList.Contains).Distinct().ToList();
}

The Distinct() does just as we would expect, it returns unique elements from a given list. So this time our resultant list will not contain duplicate values:

4, 7, 9

We can also remove duplicates using the Except() extension method.

The Except Extension Method

To produce a set difference of two sequences, we can use the Except() extension method:

public static List<int> FilterNotContainedUsingExcept(List<int> listToFilter, List<int> filteringList)
{
    return listToFilter.Except(filteringList).ToList();
}

This method also returns a filtered list that does not contain the elements from List 2:

1, 2, 3, 5, 6, 8

In our examples, the two lists contained elements of the same type. But it doesn’t have to be the case, as we’re about to see.

Filtering Lists of Different Types

The two lists may contain elements of different types. Here’s an example with a list of strings and then a filtering list of integers:

List<string> stringsToFilter = ["2", "5", "10", "20"];
filteringIntList = [5, 8, 12, 20];

Now we can filter the first list for elements that have their counterparts in the second list:

public static List<string> FilterStringsByInts(List<string> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(x => filteringList.Contains(int.Parse(x))).ToList();
}

Here, we Parse() each of the strings into an integer value and check if that value is contained within the filteringList. To keep the example simple, we are intentionally ignoring the potential issue of bad data (i.e. non-integer strings) being supplied in our listToFilter.

The filtered list now only contains those elements that have their integer counterparts in the filtering list:

5, 20

Whether we use lists of elements of identical or different types, we can filter them by custom criteria as well.

Filter a List Using Custom Criteria

We don’t have to filter a list by the elements of another list themselves. Instead, we can use the elements of the filtering list as a criterion.

To illustrate this, let’s create a list of strings and a list of integers:

List<string> animalsToFilter = ["cat", "fox", "horse", "donkey", "snake", "lion"];
List<int> filteringLengthList = [3, 5];

The first list contains the names of animals. The second list contains some integers. Now, let’s keep only the items in List 1 whose string lengths are equal to the numbers in List 2:

public static List<string> FilterAnimalNamesByLengths(List<string> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(animalName => filteringList.Any(length => animalName.Length == length))
                       .ToList();
}

Our resultant list contains only the names that consist of three or five characters:

cat, fox, horse, snake

We can also filter lists containing complex objects. Let’s have a look at that next.

Filter a List of Complex Objects

We don’t have to limit ourselves to simple data types when filtering lists. We can also filter lists of complex objects. Let’s start by defining two classes:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int SchoolId { get; set; }
    public string City { get; set; }

    override public string ToString()
    {
        return $"{Name} from {City}";
    }
}

public class School
{
    public int Id { get; set; }
    public string City { get; set; }
}

Now let’s define two lists of objects:

List<Student> studentsToFilter =
[
    new Student { Id = 1, Name = "Alice Straw", SchoolId = 1, City = "Munich" },
    new Student { Id = 2, Name = "Mike McAllen", SchoolId = 1, City = "Munich" },
    new Student { Id = 3, Name = "Bettany Kelly", SchoolId = 2, City = "Essen" },
    new Student { Id = 4, Name = "Sue Heck", SchoolId = 2, City = "Wuppertal" },
    new Student { Id = 5, Name = "Jake Millner", SchoolId = 2, City = "Mainz" },
    new Student { Id = 6, Name = "Allen Claude", SchoolId = 3, City = "Hamburg" }
];

List<School> schoolsToFilter =
[
    new School { Id = 1, City = "Munich" },
    new School { Id = 2, City = "Essen" },
    new School { Id = 3, City = "Hamburg" }
];

Now, let’s filter the first list by the students who attend a school in the same city they live in:

public static List<Student> FilterStudentsBySchoolCity(List<Student> listToFilter, List<School> filteringList)
{
    return listToFilter.Where(student => filteringList.Any(school => student.City == school.City))
                       .ToList();
}

Four out of the six students attend a school in the city where they live:

Alice Straw from Munich, Mike McAllen from Munich, Bettany Kelly from Essen, Allen Claude from Hamburg

We can also use specific properties when we filter a list. 

Selecting Specific Properties

We can filter a list and create a list of objects by selecting specific properties. Let’s use the classes we defined before and create a list of students again. But this time, our resulting list will contain only the names of students who attend one of the schools from list 2. We represent the schools in List 2 by their IDs:

List<int> schoolIds = [1, 3];

So, we’re just going to select a specific property of the object, not the entire object. In our case, we select the Name property:

public static List<string> FilterStudentsWithProperties(List<Student> listToFilter, List<int> filteringList)
{
    return listToFilter.Where(student => filteringList.Contains(student.SchoolId))
                       .Select(student => student.Name).ToList();
}

The method returns a list containing the names of the students who attend the schools with Ids 1 or 3:

Alice Straw, Mike McAllen, Allen Claude

Besides selecting particular properties, we can update properties. Let’s see how to do it.

Updating Properties Using a Filter

Let’s say the students with specified IDs move to Berlin. Using the ForEach() method, we can update their City property:

public static void UpdateCityProperty(List<Student> listToFilter, List<int> filteringList)
{
    listToFilter.ForEach(student =>
    {
        if (filteringList.Contains(student.Id))
            student.City = "Berlin";
    });
}

If we now pass a list with IDs 4 and 6 as the second argument, the City property of the students with these IDs will be updated:

Alice Straw from Munich, Mike McAllen from Munich, Bettany Kelly from Essen, Sue Heck from Berlin, Jake Millner from Mainz, Allen Claude from Berlin

If we want to filter a list more efficiently, we can consider using a HashSet.

Filter a List via HashSet

Using a HashSet and set operations can be an effective and efficient way of filtering lists. The reasons behind this are outside the scope of this article, so for a deeper dive, don’t miss our articles HashSet in C# and HashSet vs SortedSet in C#.

In our example below we’re using the IntersectWith() method. This method is specific to HashSet, and it should be noted, that it modifies the existing HashSet. It’s also worth mentioning that HashSet, being a set, will contain only unique values. This means that, for example, if we have a HashSet<int> and add the number 42 to it 4 times, it will only contain one instance of it. 

Now, let’s have a look at how a HashSet can be used to filter a list. First, let’s redefine our two lists of integers:

intsToFilter = [2, 4, 6, 8, 10, 16, 20, 28, 40, 45];
filteringIntList = [0, 10, 20, 30, 40, 50];

Now we want to filter the first list so that it only contains the elements that are also in list 2:

public static List<int> FilterUsingHashSet(List<int> listToFilter, List<int> filteringList)
{
    HashSet<int> hashToFilter = new HashSet<int>(listToFilter);
    hashToFilter.IntersectWith(filteringList);

    return hashToFilter.ToList();
}

When we execute our code, we get the elements that are in both collections:

10, 20, 40

Last but not least, let’s see how to use masks to filter lists.

Using Masks to Filter a List

Generally, masks are lists of boolean values used to include or exclude data from a stream or collection. In our case, we can use them to filter lists.

Let’s redefine our list of integers and create a mask:

intsToFilter = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
List<bool> mask = [true, false, false, false];

We can treat the mask as a cyclic pattern. If the mask contains fewer items than the list, we simply cycle over it. So, in our case, the pattern is:

true, false, false, false, true, false, false, false, true, false, false, false...

We compare the elements in the first list and the mask index by index:

public static List<int> FilterUsingMask(List<int> listToFilter, List<bool> mask)
{ 
    return listToFilter.Where((item, index) => mask[index % mask.Count]).ToList();
}

With this approach, only the integers that correspond to the value true in the mask are retained:

1, 5, 9, 13

Conclusion

Filtering lists is a common operation. In this article, we looked at different ways of filtering a list based on a second list. While not exhaustive, our examples covered the most typical cases. What did we miss? Let us know in the comments below!

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!