Skip to main content

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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
// 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:

csharp
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:

csharp
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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
// 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#:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

  1. Create a generic ISorter<T> interface with a method Sort(IList<T> items) and implement it with different sorting algorithms (bubble sort, quick sort).

  2. Design a generic event system using interfaces like IEventPublisher<T> and IEventSubscriber<T>.

  3. Implement a generic IValidator<T> interface with a method ValidationResult Validate(T item) and create implementations for different entity types.

  4. Create a generic caching system with ICache<TKey, TValue> interface and implement both memory and file-based cache providers.

  5. Design a generic repository system with IRepository<T>, IReadOnlyRepository<T>, and ISearchableRepository<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! :)