C# Generic Constraints
C# generic constraints let you tell the compiler what kinds of types may be used for a generic type parameter. They matter because unconstrained T is almost unknown: you can store it, pass it around, compare it to null only in limited cases, and call members from object, but you cannot safely call custom methods. A constraint turns a generic placeholder into a promise, such as “T must implement IComparable<T>” or “T must have a public parameterless constructor.”
Overview: How Generic Constraints Work
A generic declaration such as static T Max<T>(T left, T right) works with many possible types. That flexibility also means the compiler must type-check the method body before it knows whether callers will use int, string, or a custom class. Without a constraint, the compiler cannot assume that T has a CompareTo method, a default constructor, an Id property, or any operator such as >.
A constraint is written in a where clause after the generic parameter list. It narrows the valid type arguments and unlocks operations inside the generic code. For example, where T : IComparable<T> means every valid T must implement that interface, so the generic method can call left.CompareTo(right). If a caller tries to use a type that does not satisfy the constraint, the compiler rejects the call.
Constraints are part of the generic member’s metadata. The compiler uses them during type checking, and the CLR also understands the constraints when loading and verifying generic code. Constraints do not create a different source copy for each type. They are compile-time and runtime rules attached to the generic definition. This is different from dynamically checking a type with is or reflection after the program starts.
The most common constraints are class for reference types, struct for non-nullable value types, notnull for non-nullable type arguments in nullable-aware code, a base class constraint, an interface constraint, and new() for public parameterless construction. C# also supports specialized constraints such as unmanaged and Enum for lower-level or enum-focused APIs.
Syntax
class Repository<TEntity> where TEntity : Entity, new()
{
public TEntity Create()
{
return new TEntity();
}
}
static T Max<T>(T left, T right) where T : IComparable<T>
{
return left.CompareTo(right) >= 0 ? left : right;
}
| Constraint | Meaning |
|---|---|
where T : class |
T must be a reference type. |
where T : struct |
T must be a non-nullable value type. |
where T : notnull |
T should be non-nullable in nullable-aware code. |
where T : BaseType |
T must inherit from the specified base class. |
where T : ISomeInterface |
T must implement the specified interface. |
where T : new() |
T must have a public parameterless constructor. It must be listed last. |
where T : unmanaged |
T must be a non-nullable value type containing no reference-type fields. |
You can apply constraints to multiple type parameters with separate where clauses. When combining constraints, put at most one primary kind such as class, struct, notnull, or a base class first, then interfaces, then new() last.
Examples
Using an Interface Constraint to Compare Values
using System;
class Program
{
static void Main()
{
Console.WriteLine(Larger(10, 25));
Console.WriteLine(Larger("pear", "apple"));
}
static T Larger<T>(T left, T right) where T : IComparable<T>
{
return left.CompareTo(right) >= 0 ? left : right;
}
}
Output:
25
pear
The method can call CompareTo because the constraint guarantees that every T implements IComparable<T>. Both int and string satisfy the constraint, so the same method works for numbers and text while still being type-safe.
Using a Base Class Constraint
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Customer> customers = new List<Customer>
{
new Customer { Id = 101, Name = "Mina" },
new Customer { Id = 102, Name = "Omar" }
};
PrintIds(customers);
}
static void PrintIds<T>(IEnumerable<T> items) where T : Entity
{
foreach (T item in items)
{
Console.WriteLine($"{item.Id}: {item.GetLabel()}");
}
}
}
class Entity
{
public int Id { get; init; }
public virtual string GetLabel()
{
return $"Entity {Id}";
}
}
class Customer : Entity
{
public string Name { get; init; } = "";
public override string GetLabel()
{
return Name;
}
}
Output:
101: Mina
102: Omar
The where T : Entity constraint lets PrintIds use Id and GetLabel. The method still accepts any future type derived from Entity, not only Customer.
Using new() to Create Generic Objects
using System;
class Program
{
static void Main()
{
AuditEntry entry = Factory.Create<AuditEntry>();
entry.Message = "User signed in";
Console.WriteLine(entry.Message);
}
}
static class Factory
{
public static T Create<T>() where T : new()
{
return new T();
}
}
class AuditEntry
{
public string Message { get; set; } = "Created";
}
Output:
User signed in
The new() constraint is required because generic code cannot call new T() unless it knows T has a public parameterless constructor. This constraint is useful for simple factories, but it only supports parameterless construction, so it is not a replacement for dependency injection or rich constructors.
Combining Constraints
using System;
class Program
{
static void Main()
{
TrackedOrder order = CreateTracked<TrackedOrder>();
Console.WriteLine(order.Id);
Console.WriteLine(order.CreatedBy);
}
static T CreateTracked<T>() where T : Entity, IAuditable, new()
{
T item = new T();
item.Id = 500;
item.CreatedBy = "system";
return item;
}
}
class Entity
{
public int Id { get; set; }
}
interface IAuditable
{
string CreatedBy { get; set; }
}
class TrackedOrder : Entity, IAuditable
{
public string CreatedBy { get; set; } = "";
}
Output:
500
system
This method needs three facts: T is an Entity, implements IAuditable, and can be constructed with new T(). The order matters: base class first, interfaces after that, and new() last.
How It Works Step by Step
- The compiler reads the generic declaration and records each type parameter, such as
TorTEntity. - It reads the
whereclauses and attaches those requirements to the generic method, class, interface, or delegate. - While compiling the generic body, it allows only operations proven by the constraints. An interface constraint permits interface members; a base class constraint permits base class members;
new()permitsnew T(). - At each call site, the compiler checks the chosen type argument against the constraints. A type that fails the rule cannot be used.
- The compiled assembly stores the generic definition and its constraints as metadata. The CLR uses that metadata when constructing and verifying closed generic types such as
Repository<Customer>. - For value types, generic code can often avoid boxing because the CLR knows the exact constructed type. Constraints add type knowledge without giving up generic performance.
Constraints do not mean the generic code can do anything a particular future type might support. The body is checked once against the declared constraints. If you need a member, express it through an interface, base class, static abstract interface member, delegate parameter, or another explicit design.
Common Mistakes
Trying to Call Members Without a Constraint
static string GetName<T>(T item)
{
return item.Name;
}
This does not compile because an unconstrained T has no guaranteed Name property. Define an interface that contains the needed member, then constrain T to that interface.
using System;
class Program
{
static void Main()
{
Product product = new Product { Name = "Keyboard" };
Console.WriteLine(GetName(product));
}
static string GetName<T>(T item) where T : INamed
{
return item.Name;
}
}
interface INamed
{
string Name { get; }
}
class Product : INamed
{
public string Name { get; init; } = "";
}
Output:
Keyboard
Putting new() in the Wrong Place
class Store<T> where T : new(), IDisposable
{
}
This does not compile because new() must appear last in the constraint list. Put interface and base class constraints before it.
using System;
class Program
{
static void Main()
{
Store<Connection> store = new Store<Connection>();
Console.WriteLine(store.Create().IsOpen);
}
}
class Store<T> where T : IDisposable, new()
{
public T Create()
{
return new T();
}
}
class Connection : IDisposable
{
public bool IsOpen { get; } = true;
public void Dispose()
{
}
}
Output:
True
Using class When You Really Mean notnull
where T : class excludes value types. That is correct when your API specifically needs reference-type behavior, but it is too narrow when you only want to avoid nullable keys or nullable values. In nullable-aware libraries, where T : notnull can accept both string and int while communicating that nullable type arguments are not intended.
Best Practices
- Add a constraint only when the generic code truly needs the capability. Unnecessary constraints make reusable APIs less useful.
- Prefer interface constraints for behavior, such as
INamed,IComparable<T>, orIDisposable. They are more flexible than forcing a shared base class. - Use base class constraints when shared state or inherited implementation is genuinely part of the contract.
- Place constraints in the required order: primary kind or base class first, interfaces next, and
new()last. - Use
structfor non-nullable value types andclassfor reference types. Usenotnullwhen both are acceptable but nullable type arguments should be avoided. - Do not use
new()for complex object creation. Prefer factories or dependency injection when construction requires parameters or services. - Keep type parameter names clear. Use
Tfor a single obvious type, and names such asTEntity,TKey, orTResultwhen the role matters. - Remember that constraints are compile-time contracts, not runtime validation messages. A failed constraint is usually a compile error, not an exception.
Practice Exercises
- Write a method
Smallest<T>that accepts two values and returns the smaller one. ConstrainTwithIComparable<T>and test it withintandDateTime. - Create an
INotifiableinterface with aSendmethod. Write a generic methodNotifyAll<T>that accepts a list ofTwhereT : INotifiable. - Build a generic
ResettablePool<T>whereT : IResettable, new(). It should create a new item, callReset, and return it.
Summary
- Generic constraints describe which types are allowed for a type parameter.
- Constraints let the compiler safely allow members such as
CompareTo, base class properties, interface methods, andnew T(). - Common constraints include
class,struct,notnull, base class, interface,unmanaged, andnew(). - Invalid type arguments fail at compile time, which keeps generic APIs type-safe.
- Use the narrowest constraint that expresses the real requirement, and prefer interfaces for behavior-focused contracts.
