← Open in the full interactive course (progress tracking, search & more)

Every where clause is a promise the compiler enforces on your behalf — pick the promise that matches exactly what your code needs, and no more.

The previous lesson gave you the overview: constraints trade "works for absolutely anything" for real capability. Now it's time for the details — the exact syntax for each kind of where constraint, what each one specifically guarantees, how to combine several at once, and a subtler case: constraining one type parameter using another.

In this lesson, you'll go deep on where T : class, where T : struct, where T : new(), where T : BaseClass, where T : IInterface, combining multiple constraints on the same type parameter, and constraining a type parameter against another type parameter.

What Is It?

A constraint clause is written as where TParam : constraint1, constraint2, ... immediately after a generic type's or method's parameter list, before the opening { (or before the method body, if there's no other clause after it). Let's walk through each kind precisely.

ConstraintMeaningUnlocks
where T : classT must be a reference typeComparing to null with reference semantics; T? means "may be null"
where T : structT must be a non-nullable value typeGuaranteed never null; no boxing surprises; can combine with default(T) safely
where T : notnullT cannot be a nullable reference or nullable value typeA lighter guarantee than class/struct — common for dictionary-style keys
where T : new()T must have an accessible public parameterless constructornew T() inside your code
where T : BaseClassT must be, or derive from, BaseClassCalling any public/protected member of BaseClass on a T value
where T : IInterfaceT must implement IInterfaceCalling any member IInterface declares

Why Does It Exist?

The Problem

Different generic code needs different guarantees. A cache that must construct a fresh default instance when a key is missing needs new(). A comparer needs an interface guarantee. A type that must never accidentally hold null needs struct. One general "constraint" concept wouldn't be precise enough — real code needs several distinct, composable guarantees, each unlocking a different specific capability.

The Solution

C# provides a small, purpose-built vocabulary of constraint kinds, and lets you combine them precisely for exactly what your code needs — no more, no less. The rest of this lesson walks through each one with a concrete example of the problem it solves.

Big Picture

COMBINING MULTIPLE CONSTRAINTS — ORDER MATTERS
where T : class, IComparable<T>, new()
         ↑         ↑                  ↑
    1. class/struct/notnull   2. base class, then   3. new() —
       (if present, comes        interfaces               always last
       first)
The required order is: a type category constraint (class/struct/notnull) first, if present — then a base class, if any — then any number of interfaces — then new(), always last. You cannot combine class and struct together (a type can't be both).

How It Works

EACH CONSTRAINT, ONE AT A TIME
1. where T : class — REFERENCE TYPES ONLY
public bool IsNull<T>(T value) where T : class
    => value is null;

IsNull<string>(null);   //  string is a reference type
// IsNull<int>(0);      //  compile error — int is a value type, not allowed
2. where T : struct — NON-NULLABLE VALUE TYPES ONLY
public T? ToNullable<T>(T value) where T : struct
    => value;   // implicit conversion to T? (Nullable<T>) is always safe here

int? maybeNumber = ToNullable(42);
// ToNullable<string>("hi");   //  compile error — string is a reference type
3. where T : new() — MUST HAVE A PUBLIC PARAMETERLESS CONSTRUCTOR
public T CreateDefault<T>() where T : new()
    => new T();   //  compiles — guaranteed a parameterless constructor exists

public class Product
{
    public Product() { }   // has a public parameterless constructor — qualifies
}
Product p = CreateDefault<Product>();
4. where T : BaseClass — MUST BE OR DERIVE FROM A SPECIFIC CLASS
public abstract class Shape
{
    public abstract double Area();
}

public double TotalArea<T>(IEnumerable<T> shapes) where T : Shape
    => shapes.Sum(shape => shape.Area());   //  Area() is guaranteed by the Shape constraint
5. where T : IInterface — MUST IMPLEMENT A SPECIFIC INTERFACE
public T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;
6. CONSTRAINING A TYPE PARAMETER AGAINST ANOTHER TYPE PARAMETER
public void Register<TBase, TDerived>(List<TBase> list, TDerived item)
    where TDerived : TBase
{
    list.Add(item);   //  TDerived is guaranteed assignable to TBase
}

List<Shape> shapes = [];
Register<Shape, Circle>(shapes, new Circle());   //  Circle : Shape

Simple Example

// Combining a type category, an interface, and the constructor constraint public class ObjectPool<T> where T : class, IResettable, new() { private readonly Stack<T> _available = new(); public T Rent() => _available.Count > 0 ? _available.Pop() : new T(); public void Return(T item) { item.Reset(); // IResettable guarantees Reset() _available.Push(item); // class guarantees reference semantics, safe to pool } } public interface IResettable { void Reset(); } public class Buffer : IResettable { public byte[] Data { get; } = new byte[1024]; public void Reset() => Array.Clear(Data); } var pool = new ObjectPool<Buffer>(); Buffer buffer = pool.Rent(); // new T() the first time — pool starts empty // ... use buffer ... pool.Return(buffer); // Reset() called, then pooled for reuse Buffer reused = pool.Rent(); // pulled from the pool this time, not newly constructed

Code → Meaning → Result:

Real-World Example

A Cache<TKey, TValue> that lazily creates a default value the first time a key is requested is a realistic use of combining notnull (a genuinely common real-world requirement for dictionary-style keys) with new() for the value type.

public class Cache<TKey, TValue> where TKey : notnull where TValue : new() { private readonly Dictionary<TKey, TValue> _entries = []; public TValue GetOrCreate(TKey key) { if (!_entries.TryGetValue(key, out TValue? value)) { value = new TValue(); _entries[key] = value; } return value; } } public class UserPreferences { public bool DarkMode { get; set; } public string Language { get; set; } = "en"; } var cache = new Cache<string, UserPreferences>(); UserPreferences prefs = cache.GetOrCreate("user-42"); // constructs a fresh default the first time prefs.DarkMode = true; UserPreferences samePrefs = cache.GetOrCreate("user-42"); // returns the same instance now Console.WriteLine(samePrefs.DarkMode); // True

Notice this lesson's constraint syntax uses one where clause per type parameter, on its own — a common, very readable style once a generic type has multiple type parameters each carrying its own constraint.

Analogy

A Rental Application's Checklist

Think of each constraint as one line on a rental application's eligibility checklist: "must have valid ID" (notnull), "must have a co-signer" (an interface), "must be able to move in immediately" (new()). Each requirement independently narrows the pool of eligible applicants, and you can stack as many as the situation genuinely calls for — but a landlord who demands ten unrelated requirements for a studio apartment is turning away tenants who would have been perfectly fine. The same restraint applies to constraints: add exactly what your code needs, not more.

Under the Hood

HOW new() IS ACTUALLY COMPILED
1. new T() BECOMES Activator.CreateInstance<T>() STYLE CODE... MOSTLY
2. INTERFACE CONSTRAINTS ARE CHECKED STRUCTURALLY AT COMPILE TIME
3. class vs struct AFFECTS HOW THE JIT SPECIALIZES THE TYPE

Common Confusion

1. class as a constraint vs class as a declaration keyword

where T : class looks like it might mean "T must literally be the type named class" — it doesn't. In this position, class is a special keyword meaning "any reference type," not a reference to a specific class. This dual meaning (declaration keyword vs constraint keyword) trips up many learners at first.

2. notnull is weaker than it sounds

where T : notnull only prevents the type argument itself from being a nullable reference type (string?) or Nullable<T> (int?) — it's a compile-time nullability check tied to nullable reference types, not a runtime guarantee that a given value can never be null. It's commonly used for dictionary key type parameters, mirroring Dictionary<TKey, TValue>'s own constraint.

3. You cannot combine class and struct on the same type parameter

These two constraints are mutually exclusive by definition — a type cannot be both a reference type and a value type — so where T : class, struct is simply not legal syntax.

Common Mistakes

Mistake 1 — Putting new() anywhere but last

Wrong — doesn't compile:

where T : new(), IComparable<T> // new() must come last

Correct:

where T : IComparable<T>, new() // new() is last

Mistake 2 — Expecting where T : struct to allow int?

MyMethod<int?>(...) against a where T : struct constraint fails to compile — int? (Nullable<int>) is technically a value type, but the struct constraint specifically excludes nullable value types. If you need to allow both int and int?, don't use struct — reconsider whether the constraint is really needed, or accept T? explicitly where relevant.

Mistake 3 — Reaching for a base class constraint when an interface would be more flexible

where T : Shape when all you actually need is an Area() method — this forces every future type to inherit from Shape specifically, even if it could have satisfied the need some other way. Prefer an interface constraint (where T : IHasArea) whenever you only need specific behavior, not a specific inheritance lineage — it keeps your generic code usable by a wider range of types.

When Should I Use It?

Use each constraint when

Reconsider when

Mental Model

class = reference type only   ·   struct = non-nullable value type only
notnull = no nullable reference or nullable value type   ·   new() = has a public parameterless constructor
BaseClass/IInterface = must be, derive from, or implement it

Remember:
· Order: type category → base class → interfaces → new() (always last).
· You can constrain a type parameter against another type parameter (where TDerived : TBase).
· Prefer interfaces over base classes when you only need specific behavior, not a specific hierarchy.

Key Takeaway


Check Your Understanding

You've gone deep on every kind of where constraint. Let's check your understanding.

1. Which of the following is a valid, correctly-ordered combination of constraints?

Show answer

Correct: B

Why B is correct: This follows the required order: type category (class) first, then interfaces, then new() last.

Why A is incorrect: new() must always be the last constraint listed, never before class.

Why C is incorrect: class and struct are mutually exclusive — a type cannot be both a reference type and a value type.

Why D is incorrect: The type category constraint (class) must come before any interface constraints, not after.

Reinforcement: Type category first, then base class, then interfaces, then new() — that fixed order is required by the compiler.

2. Why does where T : struct reject int? (Nullable<int>) as a type argument, even though Nullable<int> is technically a value type?

Show answer

Correct: B

Why B is correct: The struct constraint is defined to mean "non-nullable value type" — Nullable<T> is deliberately carved out, even though it's implemented as a value type, because it defeats the "guaranteed never null" guarantee the constraint is meant to provide.

Why A is incorrect: int? is a perfectly real, commonly used C# type — it's excluded specifically from the struct constraint, not invalid as a type generally.

Why C is incorrect: struct accepts any non-nullable value type, including custom structs, not just int and not string (which is a reference type).

Why D is incorrect: This is standard, documented, intentional C# behavior — not a bug.

Reinforcement: struct means "guaranteed non-nullable value type" — a deliberate, narrower guarantee than "any value type."

3. What does where TDerived : TBase mean in public void Register<TBase, TDerived>(List<TBase> list, TDerived item) where TDerived : TBase?

Show answer

Correct: B

Why B is correct: As shown in "How It Works," a type parameter can be constrained against another type parameter — this guarantees whatever is supplied as TDerived is compatible with (assignable to) whatever is supplied as TBase, which is exactly what lets list.Add(item) compile safely.

Why A is incorrect: This is valid, well-supported C# syntax — constraining one type parameter by another is a real, if less common, pattern.

Why C is incorrect: They don't need to match exactly — TDerived just needs to be TBase or something derived from/implementing it, e.g. TBase = Shape, TDerived = Circle.

Why D is incorrect: This reverses the relationship — the constraint reads TDerived : TBase, meaning TDerived is the more specific one, not the other way around.

Reinforcement: Constraining one type parameter against another keeps two independently-supplied types provably compatible with each other.

4. A generic Cache<TKey, TValue> needs to construct a brand-new TValue whenever a key is missing, and needs TKey to be safely usable as a dictionary key. Which constraints fit best?

Show answer

Correct: B

Why B is correct: This exactly matches the Cache<TKey, TValue> real-world example — notnull on the key type (mirroring Dictionary<TKey,TValue>'s own requirement), and new() on the value type so a fresh default can be constructed on demand.

Why A is incorrect: This swaps the constraints backwards — the key needs the nullability guarantee, and the value needs the constructor guarantee, not the other way around.

Why C is incorrect: Without new(), new TValue() wouldn't compile at all — some constraint is required here.

Why D is incorrect: struct would needlessly exclude perfectly valid reference-type keys and values (like string keys or a class-based value), and doesn't provide either of the two specific guarantees actually needed here.

Reinforcement: Match each constraint to the exact capability your code relies on — here, "safe dictionary key" and "constructible on demand" are two separate, distinct needs.

You now know every kind of generic constraint, and how to combine them precisely. Next: covariance and contravariance — how out and in let generic interfaces flex safely along inheritance hierarchies.


dotnetmadeeasy.com — Learn C# and .NET, the right way.