C# Generic Interfaces
Introduction
In C#, interfaces define contracts that classes can implement. Generic interfaces take this concept further by allowing you to create flexible, reusable interface definitions that can work with different data types. This powerful feature enables you to design more reusable and type-safe code.
In this tutorial, you'll learn:
- What generic interfaces are
- How to declare generic interfaces
- How to implement generic interfaces
- Common patterns and real-world applications
What Are Generic Interfaces?
A generic interface is an interface that uses type parameters, allowing it to adapt to different data types while maintaining type safety. Just like generic classes, generic interfaces enable you to write code that works with various types without sacrificing compile-time type checking.
Here's the basic syntax for defining a generic interface:
public interface IInterfaceName<T>
{
// Method declarations, properties, etc.
T GetValue();
void SetValue(T value);
}
The T
is a type parameter that will be specified when the interface is implemented.
Declaring Generic Interfaces
Let's start with a simple example of a generic interface:
public interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T item);
void Update(T item);
void Delete(int id);
}
This IRepository<T>
interface defines a contract for basic CRUD (Create, Read, Update, Delete) operations for any type T
. The generic parameter T
represents the type of entity that will be stored and retrieved.
Multiple Type Parameters
Generic interfaces can also have multiple type parameters:
public interface IConverter<TInput, TOutput>
{
TOutput Convert(TInput input);
}
This interface defines a contract for converting one type (TInput
) to another type (TOutput
).
Constraints on Generic Interfaces
You can apply constraints to the type parameters in a generic interface, just like with generic classes:
public interface IEntityProcessor<T> where T : class, IEntity, new()
{
void Process(T entity);
T Create();
}
In this example, the type T
must be:
- A reference type (
class
) - Implement the
IEntity
interface - Have a parameterless constructor (
new()
)
Implementing Generic Interfaces
Basic Implementation
Here's how to implement a generic interface:
// Implementing the generic interface
public class Repository<T> : IRepository<T>
{
private List<T> _items = new List<T>();
private int _nextId = 1;
public T GetById(int id)
{
// In a real implementation, we would search by ID
return _items.FirstOrDefault();
}
public IEnumerable<T> GetAll()
{
return _items;
}
public void Add(T item)
{
_items.Add(item);
}
public void Update(T item)
{
// Implementation for update
}
public void Delete(int id)
{
// Implementation for delete
}
}
Implementing with a Specific Type
You can also implement a generic interface with a specific type:
public class UserRepository : IRepository<User>
{
private List<User> _users = new List<User>();
public User GetById(int id)
{
return _users.FirstOrDefault(u => u.Id == id);
}
public IEnumerable<User> GetAll()
{
return _users;
}
public void Add(User item)
{
_users.Add(item);
}
public void Update(User item)
{
var index = _users.FindIndex(u => u.Id == item.Id);
if (index >= 0)
{
_users[index] = item;
}
}
public void Delete(int id)
{
_users.RemoveAll(u => u.Id == id);
}
}
Multiple Interface Implementation
A class can implement multiple generic interfaces:
public class DataProcessor<T> : IRepository<T>, IValidator<T>
{
// Implementation of IRepository<T>
public T GetById(int id) { /* ... */ }
public IEnumerable<T> GetAll() { /* ... */ }
public void Add(T item) { /* ... */ }
public void Update(T item) { /* ... */ }
public void Delete(int id) { /* ... */ }
// Implementation of IValidator<T>
public bool Validate(T item) { /* ... */ }
}
Practical Examples
Example 1: Building a Generic Repository Pattern
The repository pattern is a common design pattern in software development, and generic interfaces are perfect for implementing it:
// First, define our entity interface
public interface IEntity
{
int Id { get; set; }
}
// Create a simple entity
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// Define a generic repository interface
public interface IRepository<T> where T : IEntity
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
// Implement a generic repository
public class Repository<T> : IRepository<T> where T : IEntity
{
private List<T> _entities = new List<T>();
public T GetById(int id)
{
return _entities.FirstOrDefault(e => e.Id == id);
}
public IEnumerable<T> GetAll()
{
return _entities;
}
public void Add(T entity)
{
if (_entities.Any(e => e.Id == entity.Id))
{
throw new ArgumentException($"Entity with ID {entity.Id} already exists");
}
_entities.Add(entity);
}
public void Update(T entity)
{
var index = _entities.FindIndex(e => e.Id == entity.Id);
if (index < 0)
{
throw new ArgumentException($"Entity with ID {entity.Id} not found");
}
_entities[index] = entity;
}
public void Delete(int id)
{
_entities.RemoveAll(e => e.Id == id);
}
}
Usage example:
// Create a repository for products
var productRepo = new Repository<Product>();
// Add some products
productRepo.Add(new Product { Id = 1, Name = "Laptop", Price = 1200.00m });
productRepo.Add(new Product { Id = 2, Name = "Mouse", Price = 25.50m });
// Get all products
var allProducts = productRepo.GetAll();
foreach (var product in allProducts)
{
Console.WriteLine($"ID: {product.Id}, Name: {product.Name}, Price: {product.Price:C}");
}
// Output:
// ID: 1, Name: Laptop, Price: $1,200.00
// ID: 2, Name: Mouse, Price: $25.50
Example 2: Generic Caching Interface
Here's an example of using generic interfaces for a caching system:
// Define a generic cache interface
public interface ICache<TKey, TValue>
{
TValue Get(TKey key);
void Set(TKey key, TValue value, TimeSpan? expiration = null);
bool Contains(TKey key);
bool Remove(TKey key);
}
// Implement an in-memory cache
public class MemoryCache<TKey, TValue> : ICache<TKey, TValue>
{
private class CacheItem
{
public TValue Value { get; set; }
public DateTime? Expiration { get; set; }
public bool IsExpired => Expiration.HasValue && DateTime.UtcNow > Expiration.Value;
}
private Dictionary<TKey, CacheItem> _cache = new Dictionary<TKey, CacheItem>();
public TValue Get(TKey key)
{
if (!_cache.ContainsKey(key))
return default;
var item = _cache[key];
if (item.IsExpired)
{
Remove(key);
return default;
}
return item.Value;
}
public void Set(TKey key, TValue value, TimeSpan? expiration = null)
{
DateTime? expirationTime = null;
if (expiration.HasValue)
{
expirationTime = DateTime.UtcNow.Add(expiration.Value);
}
_cache[key] = new CacheItem
{
Value = value,
Expiration = expirationTime
};
}
public bool Contains(TKey key)
{
if (!_cache.ContainsKey(key))
return false;
var item = _cache[key];
if (item.IsExpired)
{
Remove(key);
return false;
}
return true;
}
public bool Remove(TKey key)
{
return _cache.Remove(key);
}
}
Usage example:
// Create a string-to-string cache
var userCache = new MemoryCache<string, string>();
// Add some data
userCache.Set("user1", "John Doe", TimeSpan.FromMinutes(30));
userCache.Set("user2", "Jane Smith");
// Retrieve data
string user1 = userCache.Get("user1");
Console.WriteLine($"User 1: {user1}");
// Check if key exists
bool hasUser3 = userCache.Contains("user3");
Console.WriteLine($"Has User 3: {hasUser3}");
// Output:
// User 1: John Doe
// Has User 3: False
Generic Interfaces in .NET Framework
The .NET Framework includes several important generic interfaces you'll likely use in your applications:
IEnumerable<T>
This is one of the most frequently used generic interfaces in C#:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
IEnumerable<T>
represents a sequence of elements that can be enumerated (iterated over). It's the foundation for LINQ and many other collection operations.
ICollection<T>
ICollection<T>
extends IEnumerable<T>
to add methods for modifying a collection:
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
IList<T>
IList<T>
extends ICollection<T>
to add methods for indexed access:
public interface IList<T> : ICollection<T>
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
IDictionary<TKey, TValue>
IDictionary<TKey, TValue>
represents a collection of key-value pairs:
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
void Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, out TValue value);
}
Covariance and Contravariance in Generic Interfaces
C# supports variance for generic interfaces, which allows for more flexible use of generic interfaces:
Covariance (out)
Covariance allows you to use a more derived type than originally specified:
public interface IProducer<out T>
{
T Produce();
}
public class Animal { }
public class Dog : Animal { }
// Because of covariance (out), this is valid:
IProducer<Dog> dogProducer = new DogProducer();
IProducer<Animal> animalProducer = dogProducer; // Legal with covariance
Contravariance (in)
Contravariance allows you to use a less derived type than originally specified:
public interface IConsumer<in T>
{
void Consume(T item);
}
// Because of contravariance (in), this is valid:
IConsumer<Animal> animalConsumer = new AnimalConsumer();
IConsumer<Dog> dogConsumer = animalConsumer; // Legal with contravariance
Summary
Generic interfaces in C# provide a powerful way to create flexible, reusable, and type-safe abstractions. They allow you to define contracts that work across different data types while maintaining compile-time type checking.
Key points to remember:
- Generic interfaces use type parameters to create flexible contracts
- Type parameters can be constrained using
where
clauses - Classes can implement generic interfaces with specific types or remain generic
- Many built-in .NET interfaces are generic (like
IEnumerable<T>
,IList<T>
, etc.) - Covariance (
out
) and contravariance (in
) provide additional flexibility
Generic interfaces are essential for many design patterns and frameworks, including LINQ, dependency injection frameworks, and the repository pattern.
Exercises
-
Create a generic
ISorter<T>
interface with a methodSort(IList<T> items)
and implement it with different sorting algorithms (bubble sort, quick sort). -
Design a generic event system using interfaces like
IEventPublisher<T>
andIEventSubscriber<T>
. -
Implement a generic
IValidator<T>
interface with a methodValidationResult Validate(T item)
and create implementations for different entity types. -
Create a generic caching system with
ICache<TKey, TValue>
interface and implement both memory and file-based cache providers. -
Design a generic repository system with
IRepository<T>
,IReadOnlyRepository<T>
, andISearchableRepository<T>
interfaces.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)