Skip to main content

⚡ Part 3 : Real-World Use Cases of Generics in C#

in Part 3, we’ll apply generics to real-world enterprise scenarios:

Generic Repository Pattern

Generic Service Layer

Generic Caching Utility

Generic API Response Wrapper

Let’s dive in 🚀


1) Generic Repository Pattern

The repository pattern is used to separate data access logic (like Add, Get, Remove) from the rest of the code.
Without a generic repository, you’d write separate repositories for each entity (CustomerRepository, OrderRepository, etc.), which leads to a lot of duplicate code.

Generics solve this by creating one reusable repository for all entities.


Code Walkthrough

// All entities will implement IEntity so that we can guarantee they have an Id property
public interface IEntity
{
    int Id { get; set; }
}

👉 Why?
Because in most systems, every object (Customer, Order, Product) has an Id that uniquely identifies it.
By enforcing IEntity, we make sure our repository can always look up objects by Id.


// Repository interface with CRUD-like operations
public interface IRepository<T> where T : class, IEntity
{
    void Add(T entity);
    void Remove(T entity);
    T GetById(int id);
    IEnumerable<T> GetAll();
}

👉 Why where T : class, IEntity?

  • class ensures that T is a reference type (not int or double).

  • IEntity ensures T has an Id.
    This gives us type safety — the compiler won’t let you use Repository<int> because int doesn’t have an Id.


// In-memory repository implementing IRepository
public class InMemoryRepository<T> : IRepository<T> where T : class, IEntity
{
    private readonly List<T> _items = new(); // Local storage (simulates DB)

    public void Add(T entity) => _items.Add(entity);

    public void Remove(T entity) => _items.Remove(entity);

    public T GetById(int id) => _items.FirstOrDefault(e => e.Id == id);

    public IEnumerable<T> GetAll() => _items;
}

👉 Why a List<T>?
Because in a real app, you’d use a database (SQL, MongoDB, etc.), but here we simulate a database in memory for learning purposes.


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

public class Order : IEntity
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
}

👉 Why both inherit IEntity?
Because that guarantees each entity has an Id property, which our repository needs.


// Usage
var customerRepo = new InMemoryRepository<Customer>();
customerRepo.Add(new Customer { Id = 1, Name = "Alice" });
customerRepo.Add(new Customer { Id = 2, Name = "Bob" });

foreach (var c in customerRepo.GetAll())
    Console.WriteLine($"Customer: {c.Name}");

👉 Why does this work?
Because Customer implements IEntity, so the generic repository knows it can safely store it and look it up by Id.

Benefit: We don’t need a CustomerRepository and an OrderRepository. One generic repository works for all.


2) Generic Service Layer

Repositories are about data access.
Services are about business logic (rules, calculations, validations).

Instead of writing CustomerService, OrderService, etc., we can again make a GenericService.


public interface IService<T> where T : class, IEntity
{
    void Add(T entity);
    T GetById(int id);
    IEnumerable<T> GetAll();
}

👉 Why another interface?
This separates the contract (what methods are available) from the implementation (how they’re done).
It also makes unit testing easier because you can swap in fake implementations.


public class GenericService<T> : IService<T> where T : class, IEntity
{
    private readonly IRepository<T> _repository;

    // Dependency Injection of repository
    public GenericService(IRepository<T> repository)
    {
        _repository = repository;
    }

    public void Add(T entity) => _repository.Add(entity);

    public T GetById(int id) => _repository.GetById(id);

    public IEnumerable<T> GetAll() => _repository.GetAll();
}

👉 Why pass in IRepository<T> instead of creating one directly?
This is Dependency Injection (DI). It makes the code more flexible:

  • In dev, you can pass InMemoryRepository<T>

  • In production, you can pass SqlRepository<T>

  • In tests, you can pass FakeRepository<T>

✅ The service doesn’t care where the data comes from — it just uses the repository.


3) Generic Caching Utility

Caching is storing data temporarily so it can be accessed quickly.
Instead of making separate caches for each type, we build one generic cache.


public class Cache<T>
{
    private readonly Dictionary<string, T> _cache = new();

    public void Add(string key, T value) => _cache[key] = value;

    public T Get(string key) => _cache.TryGetValue(key, out var value) ? value : default;

    public bool Exists(string key) => _cache.ContainsKey(key);
}

👉 Why use Dictionary<string, T>?

  • string = key (unique identifier like “user:123”).

  • T = the actual cached object (could be Customer, Order, string, etc.).

👉 Why return default if key not found?

  • If T is a class → default is null.

  • If T is int → default is 0.
    This prevents exceptions when the key doesn’t exist.


var stringCache = new Cache<string>();
stringCache.Add("greeting", "Hello Generics!");
Console.WriteLine(stringCache.Get("greeting")); // Hello Generics!

var customerCache = new Cache<Customer>();
customerCache.Add("cust1", new Customer { Id = 1, Name = "David" });
Console.WriteLine(customerCache.Get("cust1").Name); // David

One cache class works for both strings and customers.


4) Generic API Response Wrapper

In APIs, every response usually contains:

  • Success/Failure flag

  • Data (could be any type)

  • Message (for errors/info)

Instead of making separate response types, we build one generic wrapper.


public class ApiResponse<T>
{
    public bool Success { get; set; }   // Did the operation succeed?
    public string Message { get; set; } // Helpful message (error or success info)
    public T Data { get; set; }         // The actual data

    // Helper for success
    public static ApiResponse<T> Ok(T data, string message = "")
        => new ApiResponse<T> { Success = true, Data = data, Message = message };

    // Helper for failure
    public static ApiResponse<T> Fail(string message)
        => new ApiResponse<T> { Success = false, Message = message };
}

👉 Why make Ok and Fail static helpers?
Because it’s easier to create responses without repeating code:

  • ApiResponse<Customer>.Ok(new Customer {...})

  • ApiResponse<Order>.Fail("Order not found")


var successResponse = ApiResponse<Customer>.Ok(new Customer { Id = 1, Name = "Emma" }, "Customer fetched");
Console.WriteLine($"Success: {successResponse.Success}, Data: {successResponse.Data.Name}");

var errorResponse = ApiResponse<Order>.Fail("Order not found");
Console.WriteLine($"Success: {errorResponse.Success}, Error: {errorResponse.Message}");

✅ One consistent response structure for all APIs, regardless of data type.


🎯 Why Write Code Like This?

  • Less Duplication → One generic repository, one service, one cache, one response wrapper.

  • More Reusability → Works for any entity type (Customer, Order, Product).

  • Type Safety → Compiler ensures correct usage (you can’t accidentally store a string in an Order repository).

  • Scalability → Adding new entities (e.g., Invoice) requires zero extra repository/service code.

  • Consistency → Every API response looks the same, every service follows the same pattern.


Link for part 4👇

👉 Next in 🔗 Part 4, we’ll cover complex scenarios like nested generics, covariance/contravariance, and performance tricks.


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