ToArray(Async) vs ToList(Async) in Entity Framework 8

10/28/2024
5 minute read

When retrieving data from your database with Entity Framework, there are two major options: ToArray and ToList. Besides the different return type, is there any significant difference in performance between the two? Let's find out!

The setup

For this to answer, I will use Sqlite with an in memory database to keep the variation as small as possible. The database will hold a BlogPost table with 10'000 records in total. Here the model:

public class BlogPost
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Subtitle { get; set; }
    public DateTime PublishDate { get; set; }
    public int Likes { get; set; }
}

The configuration of the rest is also pretty minimal:

public class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
{
    public void Configure(EntityTypeBuilder<BlogPost> builder)
    {
        builder.Property(b => b.Title)
            .HasMaxLength(4000)
            .IsRequired();

        builder.Property(b => b.Subtitle)
            .HasMaxLength(4000)
            .IsRequired();
    }
}

I also created a little data seeder to fill the database with some random data:

public static class DataSeeder  
{  
    public static void Seed(BlogContext context, int numberOfPosts)  
    {  
        var blogPosts = new List<BlogPost>();  
        for (var i = 1; i <= numberOfPosts; i++)  
        {  
            blogPosts.Add(new BlogPost  
            {  
                Title = $"Title {i}",  
                Subtitle = $"Subtitle {i}",  
                PublishDate = DateTime.Now.AddDays(-i),  
                Likes = i  
            });  
        }  

        context.BlogPosts.AddRange(blogPosts);  
        context.SaveChanges();  
    }  
}

The Benchmark

The main methods I want to test are:

[Benchmark]  
public async Task<List<BlogPost>> ToListAsyncBenchmark()  
{  
    return await _context!.BlogPosts.Take(NumberOfElements).ToListAsync();  
}  

[Benchmark]  
public async Task<BlogPost[]> ToArrayAsyncBenchmark()  
{  
    return await _context!.BlogPosts.Take(NumberOfElements).ToArrayAsync();  
}  

And some ceremonial code around:

[MemoryDiagnoser]
public class ToArrayVsToListBenchmark  
{  
    private BlogContext? _context;
    private DbConnection? _connection;

    [GlobalSetup(Targets = [nameof(ToArrayAsyncBenchmark), nameof(ToListAsyncBenchmark)])]  
    public void Setup()  
    {
        _connection = CreateInMemoryConnection();
        var options = new DbContextOptionsBuilder()
            .UseSqlite(_connection)
            .Options;
        _context = new BlogContext(options);  
        _context.Database.EnsureDeleted();  
        _context.Database.EnsureCreated();  

        DataSeeder.Seed(_context, 10000);
    }  

    [Params(100, 1000, 10000)]  
    public int NumberOfElements { get; set; } 

Let's run that bad boy!

The Results

BenchmarkDotNet v0.14.0, macOS Sequoia 15.0 (24A335) [Darwin 24.0.0]
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100-rc.1.24452.12
  [Host]     : .NET 8.0.8 (8.0.824.36612), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 8.0.8 (8.0.824.36612), Arm64 RyuJIT AdvSIMD


| Method                | NumberOfElements | Mean        | Error     | StdDev    | Gen0     | Gen1    | Gen2   | Allocated  |
|---------------------- |----------------- |------------:|----------:|----------:|---------:|--------:|-------:|-----------:|
| ToListAsyncBenchmark  | 100              |    40.67 us |  0.494 us |  0.438 us |   3.7842 |       - |      - |   30.95 KB |
| ToArrayAsyncBenchmark | 100              |    41.54 us |  0.637 us |  0.565 us |   3.7842 |       - |      - |   31.83 KB |
| ToListAsyncBenchmark  | 1000             |   332.61 us |  2.188 us |  1.940 us |  31.7383 |  0.9766 |      - |     263 KB |
| ToArrayAsyncBenchmark | 1000             |   330.59 us |  5.548 us |  5.190 us |  32.7148 |  0.9766 |      - |  270.91 KB |
| ToListAsyncBenchmark  | 10000            | 3,355.47 us | 36.660 us | 34.292 us | 320.3125 | 39.0625 | 7.8125 | 2682.84 KB |
| ToArrayAsyncBenchmark | 10000            | 3,408.63 us | 40.163 us | 35.603 us | 328.1250 | 39.0625 | 7.8125 |  2761.1 KB |

All in all, the difference is negligible. The ToList method is slightly faster with a bit less memory. The reason might be that the ToArray method has to trim the array to the correct size, and therefore has to make a copy of the array. But just with that one simple object and random values, it isn't statistically significant.

So overall, you can use both methods without worrying about performance too much. Just use the one that fits your needs better. If you need a List, use ToList, if you need an array, use ToArray. Simple as that!

Resources

Edit: see the comment from @aunikitin:

Probably, it worth watching what is going underhood and life become match more simple:

public static async Task<TSource[]> ToArrayAsync(
    this IQueryable source,
    CancellationToken cancellationToken = default)
    => (await source.ToListAsync(cancellationToken).ConfigureAwait(false)).ToArray();

Source: https://github.com/dotnet/efcore/blob/96d1997063fbe096741dc9a2bd56edbf6f55dce5/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs#L2333-L2334

Entity Framework - Storing complex objects as JSON

From time to time, it is nice to store complex objects or lists as JSON in the database. With Entity Framework 8, this is now easily possible. But this was possible all along with Entity Framework 7.

Be careful with ToListAsync and ToArrayAsync in Entity Framework Core

Entity Framework has two methods for converting a query to a list or an array: ToListAsync and ToArrayAsync. Of course there is also the sync version of these methods: ToList and ToArray. And sometimes, you should use the latter one!

Entity Framework 8: Raw SQL queries on unmapped types

The next iteration of Entity Framework, namely Entity Framework 8, will have a new and exciting feature:

Support raw SQL queries without defining an entity type for the result

That means less boilerplate code!

An error has occurred. This application may no longer respond until reloaded. Reload x