A generic type parameter without constraints is almost useless inside the method body. You can't call Compare, you can't write new T(), you can't even call .Length. Constraints are the where clauses that tell the compiler "this T is at least this kind of thing", which unlocks the operations that kind of thing supports. This lesson covers every standard constraint, what each one unlocks, the rules for combining them, and the compiler errors that show up when you get them wrong.
When you write class Repository<T> with no constraint, the compiler treats T as if it were object. That's the safest assumption it can make, because any concrete type you eventually plug in derives from object. The downside is that object has almost no useful members: ToString, GetHashCode, Equals, GetType. Nothing about ordering, no parameterless constructor guarantee, no specific properties.
Neither line compiles. The compiler doesn't know whether T has a parameterless constructor, and it doesn't know whether T implements IComparable<T>. From its point of view, T could be int, string, Stream, or some sealed class with no public constructor. It refuses to assume.
Constraints fix this by narrowing what T can be. Each constraint is a promise: "callers, you can only plug in types that satisfy this rule." In exchange, the compiler lets you use the operations that rule guarantees.
The where T : new() clause is the contract. The repository now refuses to compile if a caller writes new Repository<Stream>(), because Stream is abstract and has no public parameterless constructor. The contract is checked at the call site, not at runtime.
Constraints are the "permissions" you grant yourself inside the generic body. No constraint, no permissions. Add class, you get permission to compare against null. Add IComparable<T>, you get permission to call CompareTo. Add new(), you get permission to call new T(). Each constraint hands you a specific capability.
The diagram shows constraints as branches off the unconstrained baseline. You can stack them; we'll get to combinations later in the lesson. For now, the takeaway is that every constraint is a small unlock.
class Constraintwhere T : class says T must be a reference type. Classes, interfaces, delegates, arrays, and string all qualify. Structs, enums, and primitive value types do not.
The constraint unlocks three things. First, you can assign null to a T variable. Without the constraint, T = null doesn't compile, because T might be int, which can't be null. Second, you can compare against null with is null or == null. Third, the type works naturally with nullable reference type annotations (T? means "nullable T" when T is a reference type).
Trying to plug a value type into a class-constrained generic is rejected at the call site:
The compiler text is worth memorizing because it shows up in real code:
When nullable reference types are enabled (the default in new .NET projects), the difference between where T : class and where T : class? matters. The first requires T to be a non-nullable reference type, so Cache<string?> produces a warning. The second allows nullable reference types as well. Most repositories use the strict version, because they're storing entities, not "maybe entities."
struct Constraintwhere T : struct is the mirror of class. It restricts T to non-nullable value types: int, decimal, DateTime, Guid, enums, custom structs. It excludes reference types and Nullable<T> itself.
The constraint unlocks one important thing: the field _current doesn't need to be nullable, because value types always have a default value. It also lets you write Nullable<T> (which itself requires where T : struct) inside the generic body.
A struct-constrained generic avoids boxing when the value type is stored or passed around inside the generic. If you wrote the same code over object, every assignment would box the value onto the heap. For hot-path code (counters, IDs, money), the struct constraint can matter.
A subtle point: where T : struct excludes Nullable<T>. If you write Counter<int?>, the compiler rejects it, because int? is Nullable<int> which is itself a struct but is specifically excluded from this constraint. The rule exists because mixing nullable-of-nullable would be a mess.
new() Constraintwhere T : new() says T must have a public parameterless constructor. The unlock is new T() inside the generic body.
The factory needs to create new Order instances, and new T() is how it does that. Without the constraint, new T() produces CS0304:
The constraint is satisfied by any type with a public parameterless constructor. That includes:
| Type | Satisfies new()? | Reason |
|---|---|---|
Order (no constructors declared) | Yes | The compiler-generated default constructor counts. |
Cart() (explicit parameterless) | Yes | The explicit one counts. |
Money(decimal amount) (only this) | No | No parameterless constructor exists. |
abstract class Entity() | No | Abstract types can't be instantiated. |
int, decimal, DateTime (value types) | Yes | Every value type has an implicit parameterless default. |
Historically, new T() under the new() constraint was lowered to Activator.CreateInstance<T>(), which uses reflection and is slow. Modern .NET runtimes recognize the pattern and emit direct IL allocation when possible, so the cost on .NET 8+ is comparable to a plain new Order(). The old advice to "avoid new T() in hot loops" is mostly outdated, but profile if you're allocating millions per second.
where T : SomeBaseClass says T must be SomeBaseClass or a type that derives from it. The unlock is full access to the public and protected members of the base class.
Inside Record, the code can call item.CreatedAt, item.Id, and item.Describe() because the constraint guarantees T is at least an Entity. Without the constraint, none of those would be visible.
There are three rules to remember about base class constraints:
where T : SomeStruct, because structs can't be inherited.sealed. A sealed class has no derived types, so the constraint would be useless.where T : Repository<Order>. We'll see this in the type-parameter-on-type-parameter section.One more subtlety: you can pass instances of T to methods that accept the base type. Inside AuditLog<T>, T is implicitly convertible to Entity, so you could call SomeMethodThatTakesEntity(item) and it would just work.
where T : ISomeInterface says T must implement that interface. The unlock is the interface's methods and properties.
The repository uses item.Id because IEntity exposes that property. Plug in any type that implements IEntity, and the repository works. Plug in something that doesn't, and the constraint is violated:
You can stack multiple interface constraints by listing them. We'll see the combined form in the multiple-constraints section.
A generic interface as a constraint is just as common, especially IComparable<T> and IEquatable<T>. Here's a classic:
The IComparable<T> constraint unlocks CompareTo. decimal, string, and DateTime all implement IComparable<T> against themselves, so the method works for all three.
notnull Constraintwhere T : notnull (added in C# 8 alongside nullable reference types) says T must be a non-nullable type. It accepts both reference types and value types, as long as the reference type isn't annotated as nullable.
This is exactly the constraint Dictionary<TKey, TValue> itself uses in modern .NET. Dictionary keys can't be null, and notnull is how the BCL enforces that. The unlock is small: you can use the type without nullable annotations and without compiler warnings about null dereferences.
The constraint is enforced as a warning, not an error, when nullable reference types are enabled. Plugging string? into a notnull-constrained generic produces a CS8714:
Compared to where T : class, notnull is broader because it accepts value types too. Compared to where T : struct, it's broader because it accepts reference types. Use it when the only thing you care about is "don't give me a nullable type."
unmanaged Constraintwhere T : unmanaged says T must be an unmanaged type: a value type whose fields are also unmanaged, with no reference types anywhere in the structure. Examples include int, double, Guid, DateTime, and structs of those.
The constraint exists mainly for low-level interop and performance code: you can use sizeof(T), stackalloc T[count], or reinterpret memory safely because the type has no GC-managed references. For everyday e-commerce work, you'll rarely write unmanaged constraints yourself, but you might consume APIs that use it (like Span<T> extension methods or MemoryMarshal).
A practical example without unsafe:
ProductCode qualifies because both its fields are int. If you added a string Name field, the constraint would fail at every call site, because string is a reference type and pulls the struct out of the unmanaged category.
where T : U)You can constrain one type parameter to be assignable from another. This shows up most often in repositories and processors that hold a base type but also accept derived ones.
The constraint where TItem : TBase says "whatever TItem is, it must derive from (or equal) TBase." Inside the processor, you can treat any TItem as a TBase and use it where TBase is expected. Combined with where TBase : Entity, you get a small hierarchy of guarantees: TItem is at least TBase, which is at least Entity.
This pattern is less common in day-to-day code than the other constraints, but it shows up in BCL signatures like OfType<TResult> and in fluent builders that need to relate two type parameters.
Most real-world generics need more than one constraint. The pattern that comes up over and over in repositories is where T : class, IEntity, new():
class: T is a reference type, so we can compare against null and use nullable annotations.IEntity: T exposes the members we need (Id, Equals, whatever).new(): We can call new T() inside factory methods.This is the standard generic constraint pattern in .NET. Entity Framework's DbSet<TEntity>, ASP.NET Core's IServiceCollection.AddScoped<T>, and dozens of internal BCL types use variations of it.
Multiple constraints must appear in a specific order. The compiler enforces it.
The order is:
class, struct, unmanaged, or notnull. They're mutually exclusive (you can't be both a struct and a class).new().Valid combinations:
Invalid combinations:
The CS0401 error message reads:
where Clauses for Different Type ParametersWhen a type has more than one type parameter, each parameter gets its own where clause. They stack, one per line, before the opening brace.
Each where clause is independent. TEntity gets one set of constraints, TKey gets another. The two clauses don't interact unless you write where TKey : TEntity or similar to connect them.
Constraints can live on either a generic type declaration or a generic method declaration. The placement determines when they apply.
Constraints on a class flow into every method of that class; you don't repeat them. Constraints on a method only apply to that method and don't restrict other methods on the same type. The most common shape mixes both: a generic class has class-level constraints, and one or two methods on it add extra constraints for special operations.
Save lives under the class-level constraint. CreateAndSave<U> adds new() on its own type parameter U, which in turn must be a T. The compiler combines the constraints transitively, so U is at least class, IEntity, T, and has new().
C# 11 introduced static abstract members on interfaces, and the BCL added a family of "numeric" interfaces under System.Numerics. The headline one is INumber<T>, which lets a generic constraint promise that T supports arithmetic operators.
Before C# 11, this was impossible without writing one method per numeric type or using dynamic (slow and error-prone). The INumber<T> constraint promises +, -, *, /, comparison, Zero, One, and CreateChecked. It's implemented by int, long, decimal, double, float, and the unsigned variants.
The trade-off is a runtime cost. Generic math operators dispatch through the interface, and although the JIT specializes the code per T, the first call to a new T involves a small one-time cost. For tight inner loops over int[], a hand-written non-generic method is still faster. For everyday business code, INumber<T> fits.
A few common constraint mistakes come up here. Each one has a fix.
What's wrong with this code?
The compiler rejects new T() with CS0304 because T has no new() constraint. Without the constraint, the compiler doesn't know whether T has a parameterless constructor.
Fix:
What's wrong with this code?
With nullable reference types enabled (the default on .NET 6+), declaring private T _cached warns because the field is non-null by signature but isn't initialized in the constructor. The class constraint allows null, but the field type doesn't.
Fix: mark the field nullable and return a nullable type.
What's wrong with this code?
Sorted<Product> fails at the call site with CS0311, because Product doesn't implement IComparable<Product>. The constraint promise is broken at instantiation, not inside Sorted.
Fix: either implement IComparable<Product> on Product, or relax the constraint and accept an IComparer<T> parameter at construction.
The second form is more flexible: you can pass Comparer<Product>.Create((a, b) => a.Price.CompareTo(b.Price)) without modifying Product.
What's wrong with this code?
Stash<string> violates the struct constraint. The error appears at the call site:
Fix: if you actually want to allow reference types too, use where T : notnull or drop the constraint entirely. If you really do need a value type, fix the call site to pass int, decimal, DateTime, etc.
A reference table. When you're staring at a generic method that almost compiles, this is the table to scan.
| Constraint | What T can be | What you unlock inside the body |
|---|---|---|
| (none) | any type | members of object only (ToString, Equals, GetHashCode, GetType) |
where T : class | reference type | assign null, compare with is null, nullable annotations (T?) |
where T : struct | non-nullable value type | no null assignment, no boxing, Nullable<T> is usable, default value is well-defined |
where T : new() | type with public parameterless ctor | new T() |
where T : notnull | any non-nullable type | nullable annotations enforced, no null reference warnings on T itself |
where T : unmanaged | unmanaged value type | sizeof(T), stackalloc T[n], pointer interop |
where T : SomeBaseClass | SomeBaseClass or derived | public and protected members of SomeBaseClass |
where T : ISomeInterface | type implementing the interface | members of the interface |
where T : T2 | something assignable to T2 | implicit conversion from T to T2 |
where T : default | (only in disambiguation) | rare; see below |
The default constraint exists for a narrow case: when a generic method overrides another and the type parameter could be either a reference or value type, the compiler sometimes can't tell whether T? means "nullable reference" or "nullable value." where T : default disambiguates. You'll see it almost never. The _default Keyword in Generics_ lesson covers default(T) in depth, including this case.
A common surprise: if you derive a generic class from another generic class, the constraint doesn't transfer automatically.
If you remove the where clause from AuditedRepository, the compiler complains that T doesn't satisfy Repository<T>'s constraint. Each generic declaration carries its own constraints, and the derived type must restate (or tighten) them.
You can tighten the constraints on the derived type, but you can't loosen them:
The tighter version is allowed because every IAuditable & IEntity is also an IEntity. The looser version is rejected because not every class is an IEntity.
Constraints affect performance in two main ways. Neither should drive your design unless you're profiling a real bottleneck.
The first is boxing. When you write a generic over where T : struct, value types are stored in their native representation. No boxing, no heap allocations. When you write a generic over where T : class or no constraint, value types passed in still get boxed at the call site if the JIT can't specialize. In practice, .NET's JIT specializes generic code per value type, so List<int> stores raw ints without boxing even though List<T> has no struct constraint.
The second is interface dispatch. Calling a method through where T : IComparable<T> is a virtual call through the interface table. For value types, the JIT can sometimes devirtualize this when it knows T concretely (Largest<int>(a, b)), turning it into a direct call. For reference types, devirtualization is harder, and the call usually goes through the interface table at runtime. The cost is small (a few nanoseconds per call) but adds up in tight loops.
A generic method constrained by IComparable<T> calls CompareTo through interface dispatch. For int, the JIT often inlines this completely. For a custom class implementing IComparable<Product>, the call is virtual. If you're sorting millions of items, profile both shapes and consider implementing IComparer<T> explicitly to avoid the dispatch.
The new() constraint deserves one more note. Calling new T() historically went through Activator.CreateInstance<T>(), which uses reflection. On older runtimes this was 100x slower than a direct new. Since .NET 7, the JIT detects the new() constraint pattern and emits direct allocation IL. On .NET 8+, new T() with the new() constraint is comparable in cost to a hand-written new SomeType().
10 quizzes