IList
AddRange
C#
.NET
programming

Why doesn't IList support AddRange

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.