Generic invariance: why most generic types are invariant, how to diagnose CS1503/CS0266 conversion errors, and the fixes (covariant interfaces, generic methods with constraints, projection).
These all fail because the types are invariant: List<Dog> and List<Animal> are unrelated even though Dog is an Animal. Classes like List<T>, Dictionary<,> and Task<T> are always invariant, and IDictionary<,> is invariant because it reads and writes values. I fix it by accepting a covariant interface like IReadOnlyList<Animal>, by making the method generic with where T : Animal, or by projecting explicitly, for example awaiting the task and returning the result.
All three failures have the same root cause: invariance. A generic type G<T> is invariant unless it's an interface or delegate whose type parameter is explicitly marked out (covariant) or in (contravariant). Invariant means G<Dog> and G<Animal> are unrelated types, even though Dog : Animal. I'd explain why each case is invariant for good reasons, then fix each one.
Variance is only safe when a type parameter is used in one direction. If T is both read and written, allowing conversion would let you put the wrong type in:
List<Animal> animals = new List<Dog>(); // imagine allowed
animals.Add(new Cat()); // a Cat inside a List<Dog> - broken
So List<T>, IList<T>, ICollection<T>, IDictionary<K,V> are invariant, and classes (including Task<T>) can never be variant in .NET.
List<Dog> to a method taking List<Animal>Fix depends on what the method does:
void Feed(IEnumerable<Animal> animals) { ... } // or IReadOnlyList<Animal> / IReadOnlyCollection<Animal>
Feed(dogs); // List<Dog> converts via covariance
void AddRescued<T>(List<T> animals, Func<T> create) where T : Animal => animals.Add(create());
Feed(dogs.Cast<Animal>().ToList()) or dogs.ConvertAll<Animal>(d => d). Note the method then modifies the copy, not dogs.Dictionary<string, List<string>> to IDictionary<string, IEnumerable<string>>IDictionary<TKey, TValue> is invariant in both parameters (you can read and write values). Even IReadOnlyDictionary<TKey, TValue> is invariant in TValue, because TryGetValue(TKey, out TValue) uses TValue in an out parameter (which is invariant).
Fixes:
var index = new Dictionary<string, IEnumerable<string>>();
index["au"] = new List<string> { "Sydney", "Perth" }; // List<string> converts to IEnumerable<string> per value
IReadOnlyDictionary<string, IEnumerable<string>> view = source.ToDictionary(kv => kv.Key, kv => (IEnumerable<string>)kv.Value);
void Print<TValues>(IReadOnlyDictionary<string, TValues> map) where TValues : IEnumerable<string>.IEnumerable<KeyValuePair<string, List<string>>> / a custom read-only interface that can be covariant.Task<Dog> where Task<Animal> is expectedTask<T> is a class → invariant.
public Task<Animal> GetAnimalAsync() => GetDogAsync(); // CS0029: cannot convert Task<Dog> to Task<Animal>
Fixes:
public async Task<Animal> GetAnimalAsync() => await GetDogAsync(); // simplest; small state machine cost
public Task<Animal> GetAnimalAsync() => GetDogAsync().ContinueWith(t => (Animal)t.Result,
TaskContinuationOptions.ExecuteSynchronously); // avoid: exception/cancellation handling is subtle
In practice, async/await is the right answer. If you control the API, consider whether the method should return Task<Dog> itself, or whether a generic method Task<T> GetAsync<T>() where T : Animal fits.
"Generic types don't inherit from each other based on their arguments. Only interfaces/delegates marked out or in support conversion, and only in the safe direction. For read-only use, accept IEnumerable<T>/IReadOnlyList<T>; for tasks, await; for dictionaries, declare the value type you need or project."
In a pricing API, a method ApplyRules(List<PricingRule> rules) forced callers with List<VolumeDiscountRule> to copy the list for every request. Changing the parameter to IReadOnlyList<PricingRule> removed the copies. Another method returned Task<Product> from a cache lookup typed Task<CachedProduct>; we made it async and awaited, which was clearer than conversion helpers.
A practical debugging question that checks variance understanding through real compiler errors.
Green flags: explains the Add-a-Cat argument; distinguishes read-only from read-write callees; offers generic-method fixes; knows Task<T> is a class.
Red flags: proposes casts; thinks IDictionary or IList is covariant; copies collections as the only answer.
| Type | Variance |
|---|---|
List<T>, Dictionary<K,V>, HashSet<T>, Task<T>, Lazy<T>, any class/struct |
Invariant (classes and structs can't be variant) |
IList<T>, ICollection<T>, IDictionary<K,V>, ISet<T> |
Invariant (read + write) |
IReadOnlyDictionary<K,V> |
Invariant (uses out TValue parameter) |
IEnumerable<out T>, IReadOnlyList<out T>, IReadOnlyCollection<out T>, IAsyncEnumerable<out T> |
Covariant |
IComparer<in T>, IEqualityComparer<in T>, Action<in T> |
Contravariant |
Func<in T, out TResult> |
Mixed |
out parameters are invariantAn out TValue parameter is written by the callee (output) but also represents a storage location of the caller's type. The CLR implements out as ref — a managed pointer to caller storage — so the callee could also read it, and the caller's variable must be exactly TValue. That makes it invariant, which is why IReadOnlyDictionary<K, out V> isn't possible with TryGetValue. Some proposals suggest a covariant IReadOnlyDictionary view without TryGetValue; you can define your own.
IEnumerable<Dog> → IEnumerable<Animal>) are reference conversions: same object, no copy, O(1).Cast<T>(), ConvertAll, ToDictionary create new collections: O(n) time and memory, and changes to the copy aren't reflected in the original.Making the consumer generic preserves the exact type, so no conversion is needed and writes remain type-safe:
public static void Normalise<T>(IList<T> items) where T : Animal { foreach (var i in items) i.Name = i.Name.Trim(); }
This works for List<Dog>, List<Cat> and List<Animal> alike.
async/await re-wraps the result in a new Task<Animal> (a small allocation and state machine).ValueTask<T> is also invariant (struct).await.Arrays are covariant for reference types (Dog[] converts to Animal[]) but unsafe: writing a Cat into an Animal[] that's really Dog[] throws ArrayTypeMismatchException. Don't use arrays to work around generic invariance.
IReadOnlyList<Animal> and with a generic method; confirm no copy happens in the first.async/await and measure the allocation with BenchmarkDotNet.using System.Collections.Immutable;
namespace Generics.Invariance;
public abstract class Animal { public abstract string Name { get; } }
public sealed class Dog(string name) : Animal { public override string Name => name; }
public static class Kennel
{
// Read only: covariant interface accepts List<Dog>
public static void PrintAll(IReadOnlyList<Animal> animals)
{
foreach (var a in animals) Console.WriteLine(a.Name);
}
// Writes the caller's element type: generic with constraint
public static void AddRescued<T>(List<T> animals, Func<string, T> create) where T : Animal
=> animals.Add(create("Rescued"));
// Dictionary values: generic over the value type
public static int CountErrors<TValues>(IReadOnlyDictionary<string, TValues> errors)
where TValues : IEnumerable<string>
=> errors.Values.Sum(v => v.Count());
public static Task<Dog> FetchDogAsync() => Task.FromResult(new Dog("Rex"));
// Task<T> is invariant: await and return
public static async Task<Animal> FetchAnimalAsync() => await FetchDogAsync();
}
public static class Program
{
public static async Task Main()
{
List<Dog> dogs = [new("Bluey"), new("Bingo")];
Kennel.PrintAll(dogs);
Kennel.AddRescued(dogs, name => new Dog(name));
var errors = new Dictionary<string, List<string>> { ["email"] = ["Required", "Invalid format"] };
Console.WriteLine(Kennel.CountErrors(errors));
Animal animal = await Kennel.FetchAnimalAsync();
Console.WriteLine(animal.Name);
ImmutableArray<Dog> immutableDogs = [.. dogs];
ImmutableArray<Animal> immutableAnimals = ImmutableArray<Animal>.CastUp(immutableDogs); // zero copy
Console.WriteLine(immutableAnimals.Length);
}
}
objectWhat happens: (List<Animal>)(object)dogs.
Why it is wrong: throws InvalidCastException — they're unrelated types.
Do instead: use covariant interfaces or generic methods.
What happens: Feed(dogs.Cast<Animal>().ToList()) where Feed adds items.
Why it is wrong: additions go to the copy, not the original list.
Do instead: make the method generic, or return the new items.
What happens: passing Dog[] as Animal[].
Why it is wrong: unsafe array covariance can throw on writes.
Do instead: use read-only generic interfaces.
ContinueWith to convert tasksWhat happens: task.ContinueWith(t => (Animal)t.Result).
Why it is wrong: wraps exceptions in AggregateException, mishandles cancellation, and may schedule on unexpected schedulers.
Do instead: await in an async method.
IReadOnlyDictionary is covariantWhat happens: expecting IReadOnlyDictionary<string, List<string>> to convert to IReadOnlyDictionary<string, IEnumerable<string>>.
Why it is wrong: TValue is invariant because of TryGetValue(out TValue).
Do instead: declare the needed value type or project.
What happens: parameters typed List<T> or Dictionary<K,V>.
Why it is wrong: blocks variance and forces copies.
Do instead: accept the least capable interface needed.
objectWhat happens: changing parameters to IEnumerable<object> to accept anything.
Why it is wrong: loses type information and requires casts.
Do instead: use a common base type or generics.
What happens: treating invariance as an arbitrary limitation. Why it is wrong: leads to unsafe workarounds. Do instead: understand that invariance prevents writing the wrong type into a collection.
Invariance errors are a design signal: the method signature asks for more capability than it uses. When a method only reads, a read-only covariant interface is the honest contract and spares every caller a copy. When it writes, a constrained generic method keeps the caller's element type. I also know the handful of structural escape hatches (ImmutableArray.CastUp, return await) so I never resort to run-time casts.
Covariance means a generic type keeps the direction of inheritance: because string is an object, IEnumerable<string> can be used as IEnumerable<object>. You declare it with out T, only on interfaces and delegates, and T may only be returned, never accepted. That is why IReadOnlyList<T> is covariant but List<T> is not: List<object>.Add(42) would break a list of strings. It only works with reference types, so IEnumerable<int> does not convert to IEnumerable<object>.
Reference-type arrays in .NET are covariant: a string[] can be treated as an object[]. The array still really holds strings, so the runtime checks every write, and storing 42 fails with ArrayTypeMismatchException. The feature exists for historical reasons (Java had it, and .NET 1.0 had no generics) and it makes array writes type-unsafe at compile time. It also adds a hidden type check to stores into arrays of non-sealed reference types, and new Span<object>(stringArray) throws. The safe alternative is generic variance: IEnumerable<out T> and IReadOnlyList<out T> are covariant because they only return values, while List<T> is invariant, so the compiler catches the mistake.
A generic method has its own type parameters, for example static T Max<T>(T a, T b) where T : IComparable<T>. The compiler usually infers T from the arguments, so you call Max(3, 7) without writing <int>. Inference only looks at arguments, never the return type, and it is all or nothing, so for something like Deserialize<T>(string json) you must write the type. I make a method generic when only that operation depends on the type, and the class generic when the object holds typed state.
How to tell it (STAR)
**S:** Some validation errors silently disappeared from API responses. **T:** Find and fix the root cause. **A:** I traced it to invariance: callers copied their dictionaries to satisfy the helper's parameter type, so the helper wrote to a copy. I aligned the helper with the real type and removed the conversions. **R:** The bug was fixed for all [number] endpoints and the code got simpler.
Last updated 30 Oct 2026.