Generic interfaces are interfaces with type parameters — IRepository<T>, ILogger<T>, IOptions<T>. They combine the flexibility of generics with the contract-enforcement of interfaces and are the backbone of ASP.NET Core’s DI system. Variance — covariance and contravariance — describes whether a generic type can be used in place of a more-general or more-specific version, and is what allows IEnumerable<string> to be assigned to a variable of type IEnumerable<object>.
Generic Interfaces
// ── Generic interface with type parameter ─────────────────────────────────
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IReadOnlyList<T>> GetAllAsync();
Task<T> CreateAsync(T entity);
Task<T> UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// A generic interface with two type parameters
public interface IConverter<TSource, TDestination>
{
TDestination Convert(TSource source);
IReadOnlyList<TDestination> ConvertAll(IEnumerable<TSource> sources);
}
// Implementing a closed (concrete) version of a generic interface
public class PostRepository : IRepository<Post>
{
private readonly AppDbContext _db;
public PostRepository(AppDbContext db) => _db = db;
public async Task<Post?> GetByIdAsync(int id)
=> await _db.Posts.FindAsync(id);
public async Task<IReadOnlyList<Post>> GetAllAsync()
=> await _db.Posts.ToListAsync();
public async Task<Post> CreateAsync(Post entity)
{
_db.Posts.Add(entity);
await _db.SaveChangesAsync();
return entity;
}
public async Task<Post> UpdateAsync(Post entity)
{
_db.Posts.Update(entity);
await _db.SaveChangesAsync();
return entity;
}
public async Task DeleteAsync(int id)
{
var entity = await _db.Posts.FindAsync(id);
if (entity is not null) { _db.Posts.Remove(entity); await _db.SaveChangesAsync(); }
}
}
// Registration in DI
// builder.Services.AddScoped<IRepository<Post>, PostRepository>();
Note: ASP.NET Core’s DI container fully supports generic type registration. You can register open generics (the pattern without a specific type argument):
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)). When code requests IRepository<Post>, the container creates EfRepository<Post>; when it requests IRepository<User>, it creates EfRepository<User>. This eliminates the need to register each entity’s repository individually — one registration covers all entity types.Tip:
ILogger<T> is the most commonly used generic interface in ASP.NET Core. The type parameter T is the class that is logging — ILogger<PostService> creates log entries tagged with the full class name. This is injected automatically by the DI container — just declare ILogger<PostService> logger as a constructor parameter and the framework provides it. You do not register it manually; ASP.NET Core registers the entire logging infrastructure including generic ILogger<T> support.Warning: Variance only applies to interfaces and delegates — not to classes. You cannot assign a
List<string> to a List<object> variable (the List<T> class is invariant) even though you can assign an IEnumerable<string> to an IEnumerable<object> variable (the IEnumerable<T> interface is covariant). This distinction matters when working with return types and parameter types in LINQ, callbacks, and generic service contracts.Covariance and Contravariance
// ── Covariance (out T) — can be used as a MORE GENERAL type ──────────────
// IEnumerable<out T> — covariant
IEnumerable<string> strings = new List<string> { "hello", "world" };
IEnumerable<object> objects = strings; // ✓ covariant assignment works!
// Because IEnumerable is read-only (only produces T values, never accepts them)
// a string IS an object, so reading strings as objects is safe
// A covariant custom interface
public interface IProducer<out T>
{
T Produce();
// void Consume(T item); // compile error — cannot use T as input with 'out'
}
// ── Contravariance (in T) — can be used as a MORE SPECIFIC type ──────────
// IComparer<in T> — contravariant
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer; // ✓ contravariant assignment works!
// A comparer that handles any object can compare strings (strings are objects)
// Action<in T> is also contravariant
Action<object> handleObject = obj => Console.WriteLine(obj);
Action<string> handleString = handleObject; // ✓ an action accepting object can accept string
// ── Why this matters in practice ──────────────────────────────────────────
// Methods returning IEnumerable<string> can be assigned to IEnumerable<object> variables
// Useful when passing collections to general-purpose utilities that expect IEnumerable<object>
Common Mistakes
Mistake 1 — Expecting variance to work with List<T> (invariant class)
❌ Wrong — compile error: List<T> is invariant:
List<string> strings = new();
List<object> objects = strings; // compile error — List is invariant!
✅ Correct — use IEnumerable<T> (covariant) when only reading:
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // ✓ IEnumerable is covariant
Mistake 2 — Using T as an input parameter in a covariant (out T) interface
❌ Wrong — compile error: cannot use out T as a method parameter:
public interface IBadProducer<out T>
{
void Consume(T value); // compile error — out T cannot be an input!