ToList and what it returns
ToList() forces immediate execution of a deferred query and copies the results into a new List<T>, a snapshot that no longer follows the source (see deferred executionDeferred executionA LINQ query does not run when you write it. It runs when you enumerate it.Opens in a new tab).
List<T> ToList<T>(IEnumerable<T> source){ var result = new List<T>(); foreach (var item in source) // this is what triggers execution result.Add(item); return result;}Return type
It returns a concrete List<T>, which has indexing, Count, Add and Remove. Stored in an IEnumerable<T> variable, the same object only supports enumeration.
List<int> list = numbers.Where(n => n > 2).ToList(); list[0] = 99; // works: List<T> supports indexinglist.Add(100); // works: List<T> supports addingConsole.WriteLine(list.Count); // works, and instantly IEnumerable<int> query = numbers.Where(n => n > 2).ToList(); query[0] = 99; // ERROR: IEnumerable has no indexerquery.Add(100); // ERROR: IEnumerable has no Add methodThe interfaces build up: IEnumerable<T> can be looped over, ICollection<T> adds Count, Add and Remove, IList<T> adds indexing, and List<T> is the class.
Where it matters
Return IEnumerable<T> when callers only iterate, and List<T> when they need indexing, counting or changes. Calling ToList() too early on a database query is a separate cost, covered in LINQ providersLINQ providers: IQueryable and IEnumerableWhere a LINQ query actually runs depends on its provider, and on IQueryable versus IEnumerable.Opens in a new tab.