Replacing ToList() calls with collection expressions in modern C#

Posted: (EET/GMT+2)

 

You might have seen this pattern, and I've relied on it heavily for many years: LINQ code often ends with a call to the ToList() extension method. This happens for example when the result needs to be materialized.

Here's an example that has worked for a very long time, and still works:

List<Employee> activeEmployees =
    employees.Where(employee => employee.IsActive).ToList();

Since C# version 12 (in November 2023), collection expressions give us another way to write this:

List<Employee> activeEmployees =
[
    .. employees.Where(employee => employee.IsActive)
];

The .. syntax is called a spread element.

It tells the compiler to generate code to enumerate the sequence and place all of its elements into the new collection.

For a short expression, it can be written on one line:

List<Employee> activeEmployees =
    [.. employees.Where(employee => employee.IsActive)];

This is similar in intent to:

List<Employee> activeEmployees =
    employees.Where(employee => employee.IsActive).ToList();

In both cases, the LINQ query is enumerated and its results are materialized into a new List<Employee>. Very useful, and the collection expression is the more modern way.

You can also use collection expressions when combining existing sequences with individual values. For example, to combine three objects and lists into one merged list:

List<Employee> employeesToProcess =
[
    manager,
    .. activeEmployees,
    temporaryEmployee
];

This creates one list containing the manager, all active employees, and finally the temporary employee. As always, the target type is important. A collection expression does not have a collection type of its own.

Given the above, this kind of code works:

List<Employee> result = [.. employees];

And this creates an array instead:

Employee[] result = [.. employees];

But you generally cannot replace the target type with var:

// not enough type information
var result = [.. employees];

The surrounding code must tell the compiler which collection type to create.

What about Entity Framework Core?

An older EF query might look like this:

List<Employee> activeEmployees =
    dbContext.Employees.Where(employee => employee.IsActive)
        .ToList();

A collection expression can also enumerate that query:

List<Employee> activeEmployees =
[
    .. dbContext.Employees.Where(employee => employee.IsActive)
];

This works because the EF query implements IEnumerable<T>. Enumerating it causes the database query to execute. However, this is synchronous database access.

In an ASP.NET Core application or other asynchronous code, this is usually still better:

List<Employee> activeEmployees =
    await dbContext.Employees
        .Where(employee => employee.IsActive)
        .ToListAsync(cancellationToken);

There is currently no collection-expression equivalent of ToListAsync(). (Maybe, we could have an async version later in a future C# version.) This also means collection expressions are not a reason to replace every ToList() call mechanically.

There is also something useful about ToList(): it makes the materialization point very obvious when reading LINQ code.

IEnumerable<Employee> query =
    employees.Where(employee => employee.IsActive);

// query is materialized here
List<Employee> result = query.ToList();

The collection-expression version is shorter:

List<Employee> result = [.. query];

Both are valid. Which one is clearer depends on the surrounding code. Good rules of thumb:

  • use collection expressions when they make collection construction easier to read
  • use .. when combining existing sequences with other elements
  • remember that [.. query] still enumerates the query
  • keep ToListAsync() for asynchronous Entity Framework queries
  • do not replace ToList() just because newer syntax exists.

For in-memory objects with LINQ, this is a nice modern alternative:

List<Employee> activeEmployees =
    [.. employees.Where(employee => employee.IsActive)];

For database queries, the older-looking version may still be the better code:

List<Employee> activeEmployees =
    await dbContext.Employees
        .Where(employee => employee.IsActive)
        .ToListAsync(cancellationToken);

Modern C# gives us more ways to express the same intent. The useful part is knowing when the new syntax improves the code, and when the familiar LINQ method still communicates the intent better.

Happy hacking!