Skip to main content

⚡ 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 Push(T item) => items[index++] = item;

    // Pop returns an item of type T
    public T Pop() => items[--index];
}

Usage:

var intStack = new Stack<int>();   // Stack of integers
intStack.Push(100);
intStack.Push(200);
Console.WriteLine(intStack.Pop()); // 200

var stringStack = new Stack<string>(); // Stack of strings
stringStack.Push("Hello");
stringStack.Push("World");
Console.WriteLine(stringStack.Pop());  // World

👉 Why is this better?

  • No duplication → One class for all types

  • Type-safe → Can’t push a string into Stack<int>

  • Faster → No boxing/unboxing


2) Generic Methods

Sometimes, you don’t need a whole generic class — just a method.


Example: Swap Two Values

public static void Swap<T>(ref T a, ref T b)
{
    // Temporary variable of type T
    T temp = a;
    a = b;
    b = temp;
}

Usage:

int x = 5, y = 10;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // x=10, y=5

string s1 = "A", s2 = "B";
Swap(ref s1, ref s2);
Console.WriteLine($"{s1}, {s2}");  // B, A

👉 Why ref?
So we can swap the original variables, not just local copies.

👉 Why generic?
Because now we don’t need to write separate swap methods for int, string, double, etc.


Example: Generic Maximum Finder

public static T Max<T>(T a, T b) where T : IComparable<T>
{
    // CompareTo() comes from IComparable<T> interface
    return a.CompareTo(b) > 0 ? a : b;
}
Console.WriteLine(Max(10, 20));           // 20
Console.WriteLine(Max("Apple", "Pear"));  // Pear

👉 Why where T : IComparable<T>?
Because not every type supports comparison.
This constraint makes sure only comparable types are allowed.


3) Constraints in Generics

Constraints restrict what types can be used with generics.

Types of Constraints:

  • where T : struct → Value types only (int, double, DateTime)

  • where T : class → Reference types only (string, custom classes)

  • where T : new() → Must have a public parameterless constructor

  • where T : BaseClass → Must inherit from a specific class

  • where T : IInterface → Must implement an interface


Example: Repository Pattern (Simplified)

// All entities in system must have an Id
public interface IEntity
{
    int Id { get; set; }
}

// Generic repository with constraint
public class Repository<T> where T : class, IEntity, new()
{
    private List<T> items = new();

    public void Add(T item) => items.Add(item);

    public T GetById(int id) => items.FirstOrDefault(i => i.Id == id);
}

Usage:

public class Customer : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

var repo = new Repository<Customer>();
repo.Add(new Customer { Id = 1, Name = "Alice" });

var customer = repo.GetById(1);
Console.WriteLine(customer.Name); // Alice

👉 Why constraints here?
Because we need an Id property to look up items.
Without where T : IEntity, we’d risk calling .Id on something that doesn’t have it.


4) Multiple Type Parameters

Sometimes one placeholder (T) isn’t enough.
We can use multiple: <TKey, TValue>.


Example: Pair (like Key-Value)

public class Pair<TKey, TValue>
{
    public TKey Key { get; set; }
    public TValue Value { get; set; }

    public Pair(TKey key, TValue value)
    {
        Key = key;
        Value = value;
    }
}

Usage:

var p1 = new Pair<int, string>(1, "One");
Console.WriteLine($"{p1.Key} - {p1.Value}"); // 1 - One

var p2 = new Pair<string, bool>("IsActive", true);
Console.WriteLine($"{p2.Key} - {p2.Value}"); // IsActive - True

👉 This is exactly how Dictionary<TKey, TValue> works internally.


5) Generic Interfaces

Interfaces can also be generic.
This makes it possible to create reusable contracts.


Example: Generic Repository Interface

public interface IRepository<T>
{
    void Add(T item);
    IEnumerable<T> GetAll();
}

// In-memory implementation
public class InMemoryRepository<T> : IRepository<T>
{
    private List<T> items = new();
    public void Add(T item) => items.Add(item);
    public IEnumerable<T> GetAll() => items;
}

Usage:

var repo = new InMemoryRepository<string>();
repo.Add("Hello");
repo.Add("World");

foreach (var item in repo.GetAll())
    Console.WriteLine(item);

👉 Why?
Because now you can create repositories for Customer, Order, Product, etc., with one interface.


6) Real-World Mini Example: Generic Logger

A logger records messages. Instead of writing separate loggers for every class, make one generic logger.


public class Logger<T>
{
    public void Log(string message)
    {
        // typeof(T).Name gives the class name
        Console.WriteLine($"[{typeof(T).Name}] {message}");
    }
}

Usage:

var customerLogger = new Logger<Customer>();
customerLogger.Log("Customer created."); // [Customer] Customer created.

var orderLogger = new Logger<Order>();
orderLogger.Log("Order placed."); // [Order] Order placed.

👉 Benefit:
Logs are automatically tagged with the type name, so you know where they came from.


🎯 Key Takeaways 

  • Generics let you write reusable, type-safe code.

  • <T> is a placeholder for a type (decided when using the class/method).

  • Generic Methods → Reusable algorithms (Swap, Max).

  • Generic Classes → Reusable structures (Stack<T>, Pair<TKey, TValue>).

  • Constraints → Keep generics safe (where T : class, IEntity).

  • Multiple Type Parameters → Support advanced structures like dictionaries.

  • Generic Interfaces → Define reusable contracts.

  • Real-world examples → Repository, Logger, API responses.


✅ With this, you have a solid foundation in generics.

Link for part 2👇
In 🔗 Part 2, we explored delegates, events, factories, strategies.

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