C# generics provide type safety and code reusability, but sometimes strict type checking can make our code less flexible. To solve this problem, C# introduces variance, which defines how generic types relate to each other.
Variance allows us to use a more derived or less derived type than originally specified in a generic interface or delegate.
In C#, variance is divided into three categories:
- Covariance (
out) - Contravariance (
in) - Invariance (default behavior)
Let’s understand each of them with practical examples.
What Problem Does Variance Solve?
Suppose we have the following classes:
public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
public string Bark { get; set; }
}
Since Dog inherits from Animal, the following assignment works:
Dog dog = new Dog();
Animal animal = dog;
However, generic types do not behave the same way:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Compilation error
Why?
Because generic collections are invariant by default.
This is where variance comes into the picture.
1. Covariance (out)
Covariance allows us to use a more derived type.
If Dog inherits from Animal, then:
IEnumerable<Dog> → IEnumerable<Animal>
This works because IEnumerable<T> only produces values.
The out keyword is used to declare covariance.
public interface IReadRepository<out T>
{
T GetById(Guid id);
IEnumerable<T> GetAll();
}
Now we can write:
IReadRepository<Dog> dogRepository = ...;
IReadRepository<Animal> animalRepository = dogRepository;
This is safe because the repository only returns objects and never accepts them as input.
Real-world Example
Framework interfaces that use covariance:
IEnumerable<out T>IEnumerator<out T>IQueryable<out T>
These interfaces only return values.
Why Can We Only Return T?
Consider:
public interface IReadRepository<out T>
{
void Add(T entity);
}
The compiler will generate an error:
Invalid variance: The type parameter 'T' must be covariantly valid.
Because a covariant interface cannot accept objects.
Rule:
✅ Allowed:
T GetById();
❌ Not allowed:
void Save(T entity);
2. Contravariance (in)
Contravariance allows us to use a less derived type.
The in keyword is used for contravariance.
public interface ICommandHandler<in TCommand>
{
void Handle(TCommand command);
}
Now:
public class AnimalHandler : ICommandHandler<Animal>
{
public void Handle(Animal animal)
{
Console.WriteLine(animal.Name);
}
}
We can assign:
ICommandHandler<Animal> animalHandler = new AnimalHandler();
ICommandHandler<Dog> dogHandler = animalHandler;
Why is this safe?
Because anything that can process an Animal can also process a Dog.
Why Can We Only Accept T?
This is invalid:
public interface ICommandHandler<in T>
{
T Create();
}
A contravariant type cannot return T.
Rule:
✅ Allowed:
void Handle(T item);
❌ Not allowed:
T Get();
3. Invariance (Default Behavior)
By default, generic types in C# are invariant.
List<Dog> dogs = new();
List<Animal> animals = dogs;
The compiler throws an error.
Why?
Imagine this were allowed:
animals.Add(new Cat());
Now the original dogs list would contain a Cat, which breaks type safety.
That is why collections such as:
List<T>Dictionary<TKey, TValue>HashSet<T>
are invariant.
Variance in Repository Pattern
In a Clean Architecture project, it is common to separate read and write operations.
Read Repository (Covariant)
public interface IReadRepository<out T>
{
Task<T?> GetByIdAsync(Guid id);
Task<IEnumerable<T>> GetAllAsync();
}
Since it only returns data, covariance works perfectly.
Write Repository (Contravariant)
public interface IWriteRepository<in T>
{
Task AddAsync(T entity);
Task DeleteAsync(T entity);
}
Since it only accepts data, contravariance is possible.
Combined Repository
This will not work:
public interface IRepository<out T>
{
Task AddAsync(T entity);
Task<T> GetByIdAsync(Guid id);
}
Because the same type parameter is used for both input and output.
The compiler forces it to remain invariant:
public interface IRepository<T>
{
Task AddAsync(T entity);
Task<T> GetByIdAsync(Guid id);
}
Variance and MediatR
MediatR internally uses variance.
For example:
public interface IRequestHandler<in TRequest, TResponse>
{
Task<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken);
}
Notice:
in TRequest
The request type is contravariant because handlers consume requests.
The response type cannot be covariant here because it is wrapped inside Task<T>.
Important Rules to Remember
| Variance Type | Keyword | Can Return T | Can Accept T |
|---|---|---|---|
| Covariance | out | ✅ | ❌ |
| Contravariance | in | ❌ | ✅ |
| Invariance | None | ✅ | ✅ |
Summary
Variance is one of the most powerful features of C# generics, but it is often misunderstood.
Understanding covariance, contravariance, and invariance helps you:
- Design cleaner APIs.
- Build reusable repositories.
- Understand MediatR internals.
- Write more flexible generic code.
- Avoid type-safety issues.
Whenever you create a generic interface, ask yourself:
- Does it only return values? → Use
out. - Does it only accept values? → Use
in. - Does it do both? → Keep it invariant.
