Skip to main content

⚡ Part 2: Advanced Generics in C#

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:

  1. Generic Delegates & Events

  2. Generic Abstract Classes

  3. Generic Comparers & Equality Checkers

  4. Factory Pattern with Generics

  5. 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

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...