How to Use Covariance with IEnumerable in C

Covariance allows you to use a more derived type than originally specified in a generic interface. In C#, IEnumerable<T> is covariant because its generic type parameter is marked with the out keyword (IEnumerable<out T>). This guide explains how covariance works with IEnumerable<T>, how to leverage it when assigning derived collections to base references, and how to implement your own custom covariant collection wrappers.

What is Covariance?

Covariance preserves assignment compatibility for generic types. If class Dog inherits from class Animal, covariance allows an instance of IEnumerable<Dog> to be assigned directly to a variable of type IEnumerable<Animal>.

This is safe because IEnumerable<out T> is a read-only interface. It only outputs data (T appears only as a return type). Because you can only read from it, reading a Dog as an Animal will never violate type safety.

In contrast, List<T> is invariant because it allows both input and output operations (like .Add(T item)). You cannot assign a List<Dog> to a List<Animal>.

Using Covariance with Built-in IEnumerable<T>

Because the .NET framework defines IEnumerable<out T> with the out parameter, covariance works automatically with reference types.

public class Animal
{
    public string Name { get; set; } = string.Empty;
}

public class Dog : Animal
{
    public void Bark() => Console.WriteLine("Woof!");
}

public class Program
{
    public static void Main()
    {
        List<Dog> dogs = new List<Dog>
        {
            new Dog { Name = "Buddy" },
            new Dog { Name = "Rex" }
        };

        // Covariance in action: List<Dog> implements IEnumerable<Dog>,
        // which can be assigned to IEnumerable<Animal>.
        IEnumerable<Animal> animals = dogs;

        PrintAnimalNames(dogs);
    }

    public static void PrintAnimalNames(IEnumerable<Animal> animals)
    {
        foreach (var animal in animals)
        {
            Console.WriteLine(animal.Name);
        }
    }
}

How to Implement a Custom Covariant Interface

To implement covariance in your own generic interfaces, you must explicitly declare the generic parameter with the out keyword.

1. Define the Covariant Interface

The out keyword specifies that T is covariant. The type T can only be used as a method return type, a property get accessor return type, or in a nested covariant position.

using System.Collections;
using System.Collections.Generic;

// The 'out' keyword marks T as covariant
public interface IReadOnlyRepository<out T> : IEnumerable<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
}

If you attempt to use T as a method argument (e.g., void Add(T entity);), the compiler will throw an error because input positions require contravariance (in T) or invariance.

2. Implement the Interface

The implementing class does not use the out keyword. Classes are always invariant in C#.

public class ReadOnlyRepository<T> : IReadOnlyRepository<T>
{
    private readonly List<T> _items;

    public ReadOnlyRepository(List<T> items)
    {
        _items = items;
    }

    public T GetById(int id) => _items[id];

    public IEnumerable<T> GetAll() => _items;

    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

3. Consume the Covariant Interface

You can now pass an instance parameterized with a derived type to a reference parameterized with a base type:

IReadOnlyRepository<Dog> dogRepo = new ReadOnlyRepository<Dog>(new List<Dog> { new Dog { Name = "Max" } });

// Valid due to covariance
IReadOnlyRepository<Animal> animalRepo = dogRepo;

Animal animal = animalRepo.GetById(0);
Console.WriteLine(animal.Name);

Limitations of Covariance

  • Reference Types Only: Covariance only applies to reference types. IEnumerable<int> cannot be converted to IEnumerable<object> because int is a value type, and value types require boxing, which changes the underlying memory layout.
  • Output Only: You cannot pass T as a parameter to any method inside a covariant interface.
  • Classes Cannot Be Covariant: Only interfaces and delegates can be declared as covariant. Concrete classes like List<T> or ReadOnlyCollection<T> cannot use the out keyword.