In Part 1, we explored the basics of generics:
-
Generic methods
-
Generic classes
-
Constraints
-
Interfaces and real-world examples
Now in Part 2, we’ll dive deeper into advanced scenarios where generics really shine.
We’ll cover:
-
Generic Delegates & Events
-
Generic Abstract Classes
-
Generic Comparers & Equality Checkers
-
Factory Pattern with Generics
-
Strategy Pattern with Generics
Let’s go step by step 🚀
1) Generic Delegates & Events
A delegate is like a pointer to a method — it stores a reference to a function so we can call it later.
When we make delegates generic, we can reuse them for multiple data types.
Example: Generic Delegate
// A delegate that takes two parameters of type T and returns T
// This can represent operations like Add, Multiply, etc.
public delegate T Operation<T>(T a, T b);
class Calculator
{
// A generic Add method that works for numbers
// Constraint: "where T : struct" ensures only value types (like int, double) can be used
public static T Add<T>(T a, T b) where T : struct
{
// 'dynamic' allows performing '+' operation without knowing the type at compile time
dynamic x = a;
dynamic y = b;
return x + y;
}
}
// Usage
Operation<int> intAdd = Calculator.Add; // Works with int
Console.WriteLine(intAdd(5, 10)); // Output: 15
Operation<double> doubleAdd = Calculator.Add; // Works with double
Console.WriteLine(doubleAdd(2.5, 3.7)); // Output: 6.2
💡 Key idea: With one generic delegate, we can support int, double, decimal, etc.
Without generics, we’d need a separate delegate for each type.
Example: Generic Event Publisher/Subscriber
Events are notifications (like a doorbell 🔔).
With generics, we can notify subscribers with any type of data.
public class EventPublisher<T>
{
// Generic event that passes data of type T when triggered
public event Action<T> OnDataPublished;
// Publish method that raises the event
public void Publish(T data)
{
// Safe invoke using ?. (if no subscriber, nothing happens)
OnDataPublished?.Invoke(data);
}
}
// Usage Example
var stringPublisher = new EventPublisher<string>();
// Subscribe to the event
stringPublisher.OnDataPublished += msg => Console.WriteLine($"Received: {msg}");
// Publish event (triggers subscribers)
stringPublisher.Publish("Hello Generics!"); // Output: Received: Hello Generics!
✅ This makes a flexible event system that can publish messages of any type.
2) Generic Abstract Classes
An abstract class is a base blueprint that cannot be instantiated.
With generics, we can create reusable base classes for many data types.
Example: Generic Repository Base
// Abstract generic repository
public abstract class Repository<T>
{
// Common storage for all derived repositories
protected List<T> items = new();
// Abstract methods (must be implemented in derived classes)
public abstract void Add(T item);
public abstract IEnumerable<T> GetAll();
}
// Customer-specific repository
public class CustomerRepository : Repository<Customer>
{
public override void Add(Customer item) => items.Add(item);
public override IEnumerable<Customer> GetAll() => items;
}
// Order-specific repository
public class OrderRepository : Repository<Order>
{
public override void Add(Order item) => items.Add(item);
public override IEnumerable<Order> GetAll() => items;
}
// Usage
var customerRepo = new CustomerRepository();
customerRepo.Add(new Customer { Id = 1, Name = "Alice" });
foreach (var c in customerRepo.GetAll())
Console.WriteLine(c.Name); // Output: Alice
✅ The abstract class + generics combo avoids code duplication.
3) Generic Comparers & Equality Checkers
When working with sorting or sets, we often need custom comparison logic.
Generics allow us to write once and reuse everywhere.
Example: Generic Comparer (for Sorting)
// Generic comparer that works for any type that implements IComparable<T>
public class GenericComparer<T> : IComparer<T> where T : IComparable<T>
{
public int Compare(T x, T y) => x.CompareTo(y);
}
// Usage
var numbers = new List<int> { 5, 3, 9, 1 };
numbers.Sort(new GenericComparer<int>());
Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 3, 5, 9
Example: Generic Equality Checker (for HashSets & Dictionaries)
public class GenericEqualityComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y) => EqualityComparer<T>.Default.Equals(x, y);
public int GetHashCode(T obj) => obj?.GetHashCode() ?? 0;
}
var set = new HashSet<string>(new GenericEqualityComparer<string>())
{
"apple", "banana", "apple" // duplicate will be ignored
};
Console.WriteLine(set.Count); // Output: 2
✅ Instead of writing multiple equality checkers, one generic class handles all.
4) Factory Pattern with Generics
The Factory Pattern is used to create objects without exposing the creation logic.
Generics let us build a flexible factory.
Example: Simple Generic Factory
public class Factory<T> where T : new() // Constraint: T must have parameterless constructor
{
public T Create() => new T();
}
// Usage
var customerFactory = new Factory<Customer>();
var customer = customerFactory.Create();
customer.Name = "John";
Console.WriteLine(customer.Name); // Output: John
✅ Now we can create objects for any type with zero boilerplate.
Example: Service Factory with Interfaces
// A base interface for all services
public interface IService
{
void Execute();
}
// Service 1
public class EmailService : IService
{
public void Execute() => Console.WriteLine("Sending Email...");
}
// Service 2
public class SmsService : IService
{
public void Execute() => Console.WriteLine("Sending SMS...");
}
// Generic Factory that only works with IService implementations
public class ServiceFactory<T> where T : IService, new()
{
public T Create() => new T();
}
// Usage
var emailFactory = new ServiceFactory<EmailService>();
emailFactory.Create().Execute(); // Output: Sending Email...
var smsFactory = new ServiceFactory<SmsService>();
smsFactory.Create().Execute(); // Output: Sending SMS...
✅ Flexible, type-safe factory for any service.
5) Strategy Pattern with Generics
The Strategy Pattern lets us plug in algorithms dynamically.
With generics, we make it type-safe.
Example: Discount Strategy
// Strategy interface
public interface IDiscountStrategy<T>
{
decimal ApplyDiscount(T order);
}
// Concrete Strategy: Percentage Discount
public class PercentageDiscount : IDiscountStrategy<Order>
{
public decimal ApplyDiscount(Order order) => order.Amount * 0.9m; // 10% off
}
// Concrete Strategy: Flat Discount
public class FlatDiscount : IDiscountStrategy<Order>
{
public decimal ApplyDiscount(Order order) => order.Amount - 50; // Flat 50 off
}
// Context class that uses strategy
public class DiscountContext<T>
{
private readonly IDiscountStrategy<T> strategy;
public DiscountContext(IDiscountStrategy<T> strategy) => this.strategy = strategy;
public decimal GetFinalPrice(T entity) => strategy.ApplyDiscount(entity);
}
// Usage
var order = new Order { OrderId = 1, Amount = 500 };
// Use percentage strategy
var percentageContext = new DiscountContext<Order>(new PercentageDiscount());
Console.WriteLine(percentageContext.GetFinalPrice(order)); // Output: 450
// Use flat discount strategy
var flatContext = new DiscountContext<Order>(new FlatDiscount());
Console.WriteLine(flatContext.GetFinalPrice(order)); // Output: 450
✅ With generics + strategy, we can support multiple algorithms without rewriting code.
🎯 Key Takeaways from Part 2
-
Generic Delegates → Reusable method references across types.
-
Generic Events → Strongly-typed event systems.
-
Generic Abstract Classes → Base logic reused for multiple types.
-
Generic Comparers/Equality → Simplify sorting & collections.
-
Generic Factory Pattern → Create objects in a type-safe way.
-
Generic Strategy Pattern → Switch algorithms dynamically, safely.
Link for part 3👇
👉 In 🔗 Part 3, we’ll focus on real-world enterprise use cases of generics:
-
Generic Repository + Unit of Work pattern
-
Generic Service Layers
-
Generic Caching Utility
-
Reusable API Response Wrappers
Comments
Post a Comment