IList
AddRange
C#
.NET
programming

Why doesn't IList support AddRange

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

IList<T> does not have an AddRange method because interfaces in .NET are designed to define the minimum contract that all implementations must support. AddRange is a convenience method on List<T> (the concrete class) that calls Add in a loop with internal optimizations. Including it in the interface would force every implementer — arrays, read-only collections, custom lists — to provide a bulk-add operation, which does not make sense for all collection types.

The Interface vs Concrete Class

csharp
1// IList<T> defines individual operations
2public interface IList<T> : ICollection<T>
3{
4    T this[int index] { get; set; }
5    int IndexOf(T item);
6    void Insert(int index, T item);
7    void RemoveAt(int index);
8}
9
10// ICollection<T> (inherited by IList<T>) provides:
11// Add(T item), Remove(T item), Clear(), Contains(T item), Count
12
13// List<T> adds convenience methods not in the interface:
14// AddRange, InsertRange, RemoveRange, RemoveAll, Sort, BinarySearch, Find, etc.

AddRange is an optimization on List<T> that pre-allocates capacity and copies elements in bulk. Requiring this on the interface would be burdensome for simple implementations.

Workaround 1: Extension Method

Create an extension method that works on any IList<T>:

csharp
1public static class IListExtensions
2{
3    public static void AddRange<T>(this IList<T> list, IEnumerable<T> items)
4    {
5        if (list is List<T> concreteList)
6        {
7            // Use the optimized built-in method
8            concreteList.AddRange(items);
9            return;
10        }
11
12        foreach (var item in items)
13        {
14            list.Add(item);
15        }
16    }
17}
18
19// Usage
20IList<int> myList = new List<int>();
21myList.AddRange(new[] { 1, 2, 3, 4, 5 });

This approach checks if the underlying type is List<T> and uses its optimized AddRange when possible, falling back to individual Add calls otherwise.

Workaround 2: Cast to List

If you know the underlying type is List<T>:

csharp
1IList<string> items = GetItems(); // Returns IList<string>
2
3if (items is List<string> list)
4{
5    list.AddRange(new[] { "a", "b", "c" });
6}
7else
8{
9    foreach (var item in new[] { "a", "b", "c" })
10    {
11        items.Add(item);
12    }
13}

Workaround 3: Use List in Method Signatures

If you control the API, accept List<T> instead of IList<T> when you need AddRange:

csharp
1// If you need AddRange, use the concrete type
2public void ProcessItems(List<int> items)
3{
4    items.AddRange(FetchMoreItems());
5}
6
7// If you only need reading/iteration, use the interface
8public void DisplayItems(IReadOnlyList<int> items)
9{
10    foreach (var item in items)
11        Console.WriteLine(item);
12}

Why .NET Interfaces Are Minimal

.NET follows the Interface Segregation Principle — interfaces should not force implementers to provide methods they do not need:

csharp
1// Arrays implement IList<T> but are fixed-size
2int[] arr = { 1, 2, 3 };
3IList<int> list = arr;
4list.Add(4); // NotSupportedException — arrays can't add elements
5
6// If IList<T> required AddRange, arrays would also need to throw here
7// list.AddRange(new[] {4, 5}); // Would also throw
8
9// Read-only wrappers implement IList<T>
10IList<int> readOnly = new ReadOnlyCollection<int>(new List<int> { 1, 2, 3 });
11readOnly.Add(4); // NotSupportedException

Adding AddRange to IList<T> would mean more implementations throwing NotSupportedException, which defeats the purpose of having a contract.

ICollection vs IList vs List

csharp
1// Hierarchy:
2// IEnumerable<T>  → iterate
3//   ICollection<T> → Add, Remove, Count
4//     IList<T>      → indexed access, Insert, RemoveAt
5//       List<T>     → AddRange, Sort, Find, BinarySearch, etc.
6
7// Choose the narrowest type for your needs:
8IEnumerable<T>    // Read-only iteration
9IReadOnlyList<T>  // Indexed read-only access
10ICollection<T>    // Add/Remove without indexed access
11IList<T>          // Full indexed mutable access
12List<T>           // When you need List-specific methods like AddRange

LINQ Alternatives

Instead of AddRange, LINQ provides ways to combine collections:

csharp
1IList<int> list1 = new List<int> { 1, 2, 3 };
2IEnumerable<int> list2 = new[] { 4, 5, 6 };
3
4// Concatenate (creates new sequence, does not modify list1)
5var combined = list1.Concat(list2).ToList();
6
7// Union (removes duplicates)
8var unique = list1.Union(list2).ToList();

Note that Concat and Union create new collections rather than modifying the original.

Common Pitfalls

  • Assuming IList<T> supports all List<T> methods: IList<T> only defines indexed access and individual add/remove. Methods like AddRange, Sort, Find, and BinarySearch are on List<T> only. Check the interface definition before calling methods.
  • Catching NotSupportedException as normal flow: Arrays and read-only collections implement IList<T> but throw on mutation methods. Instead of catching exceptions, check ICollection<T>.IsReadOnly before calling Add.
  • Overusing List<T> in APIs: Exposing List<T> in public API signatures locks callers into that implementation. Use IList<T> or IReadOnlyList<T> for parameters and return types, and only use List<T> internally when you need its specific methods.
  • Extension method performance: An extension method that calls Add in a loop does not pre-allocate capacity. For large collections, this causes repeated array resizing. The workaround of casting to List<T> when possible avoids this overhead.
  • Forgetting IReadOnlyList<T>: When you only need to read from a list, use IReadOnlyList<T> instead of IList<T>. It communicates intent clearly and prevents accidental mutation.

Summary

  • IList<T> omits AddRange because interfaces define minimal contracts, not convenience methods
  • AddRange is a List<T>-specific optimization that pre-allocates and bulk-copies
  • Use an extension method to add AddRange to IList<T>, with a fast path for List<T>
  • Choose the narrowest interface type: IEnumerable<T> for reading, IList<T> for mutation, List<T> for bulk operations
  • Check IsReadOnly before calling mutation methods on IList<T> — arrays and read-only wrappers throw on Add
  • Prefer IReadOnlyList<T> when the consumer does not need to modify the collection

Course illustration
Course illustration

All Rights Reserved.