Generic Classes and Methods — Writing Once, Using Many Types

Generics let you write a class or method once and use it safely with many different types, without code duplication or the performance overhead of boxing. Before generics (pre .NET 2.0), collections stored object — every value type was boxed on insert and required an unsafe cast on retrieval. With generics, List<Post> stores only Post objects — no boxing, no casting, and the compiler catches type errors at compile time. Generics are the foundation of the entire .NET collections library and of every pattern you will use in the ASP.NET Core Web API chapters.

Generic Classes

// A generic wrapper that adds "optional value" semantics to any type
// T is the type parameter — a placeholder for the real type used later
public class Optional<T>
{
    private readonly T?   _value;
    private readonly bool _hasValue;

    private Optional(T? value, bool hasValue)
    {
        _value    = value;
        _hasValue = hasValue;
    }

    // Factory methods — more expressive than constructors
    public static Optional<T> Some(T value) => new(value, true);
    public static Optional<T> None()        => new(default, false);

    public bool HasValue => _hasValue;

    public T Value =>
        _hasValue ? _value! : throw new InvalidOperationException("No value present.");

    public T ValueOrDefault(T defaultValue) => _hasValue ? _value! : defaultValue;

    public override string ToString() => _hasValue ? $"Some({_value})" : "None";
}

// Using Optional<T> with different concrete types
Optional<string>  name   = Optional<string>.Some("Alice");
Optional<int>     score  = Optional<int>.Some(95);
Optional<Post>    post   = Optional<Post>.None();

Console.WriteLine(name.Value);               // "Alice"
Console.WriteLine(score.ValueOrDefault(0));  // 95
Console.WriteLine(post.HasValue);            // false

// Type safety — compiler rejects wrong types at compile time
// Optional<string> wrong = Optional<string>.Some(42);  // compile error!
Note: The .NET runtime creates a separate native code version of each generic type instantiation for value types (so List<int> and List<double> have separate native implementations), but shares one implementation for all reference type instantiations (so List<Post> and List<User> share the same native code). This is why generics with value types are completely free of boxing overhead, while generics with reference types have the same performance as working directly with object but with the safety of compile-time type checking.
Tip: The convention for type parameter names in C# is a single uppercase letter, usually T for a single type parameter. When a class has multiple type parameters, use TKey and TValue (as in Dictionary<TKey, TValue>), or descriptive names like TEntity, TResult, TRequest. Avoid cryptic single letters beyond T, K, and V — for domain-specific generics, a descriptive name like TEntity is far clearer than E.
Warning: Do not confuse generic type parameters with generic constraints. public class Repo<T> declares a type parameter named T. where T : class constrains what types can be substituted for T. Without constraints, you can only use operations available on object inside the generic body — you cannot call T-specific methods unless you constrain T to an interface or base class that declares those methods. The next lesson covers constraints in depth.

Generic Methods

// Generic method — T is inferred from the argument type
public static class Extensions
{
    // Swap two variables — works for any type
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a; a = b; b = temp;
    }

    // Safe null coalescing for any reference type
    public static T OrDefault<T>(this T? value, T defaultValue) where T : class
        => value ?? defaultValue;

    // Convert a list to a dictionary with a key selector
    public static Dictionary<TKey, TValue> ToKeyedDictionary<TKey, TValue>(
        this IEnumerable<TValue> source,
        Func<TValue, TKey>       keySelector) where TKey : notnull
        => source.ToDictionary(keySelector);

    // Paginate any IQueryable
    public static async Task<PagedResult<T>> ToPagedResultAsync<T>(
        this IQueryable<T> query, int page, int pageSize)
    {
        int total = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
        return new PagedResult<T>(items, total, page, pageSize);
    }
}

// Type inference — no need to specify <int> explicitly
int x = 1, y = 2;
Extensions.Swap(ref x, ref y);   // compiler infers T = int
Console.WriteLine($"x={x}, y={y}");  // x=2, y=1

// Method type argument explicitly specified when inference is ambiguous
var dict = new List<Post>().ToKeyedDictionary<int, Post>(p => p.Id);

Generics vs object (The Pre-Generic Way)

// Pre-generics: ArrayList stored object — unsafe, slow
var oldList = new System.Collections.ArrayList();
oldList.Add(42);
oldList.Add("oops");   // accepted — no type safety!
int n = (int)oldList[0];  // required cast — could throw InvalidCastException
// int m = (int)oldList[1]; // throws InvalidCastException at runtime!

// With generics: compile-time safety, no boxing for value types
var newList = new List<int>();
newList.Add(42);
// newList.Add("oops");  // ← compile error! Type safety enforced
int m = newList[0];       // no cast needed

Common Mistakes

Mistake 1 — Using object instead of generics for reusable code

❌ Wrong — loses type safety and incurs boxing for value types:

public class Pair { public object First { get; set; } public object Second { get; set; } }

✅ Correct — use generics:

public class Pair<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } }

Mistake 2 — Specifying type arguments explicitly when the compiler can infer them

❌ Verbose — type argument is obvious from the argument:

Extensions.Swap<int>(ref x, ref y);

✅ Concise — let the compiler infer:

Extensions.Swap(ref x, ref y);

🧠 Test Yourself

Why does List<int> have better performance than an ArrayList storing integers?