Generics Constraints in C#: Writing Precise Generic Code
Generics let you write code that works with any type. Constraints let you write code that works with any type that meets specific requirements. Without constraints, a generic type parameter is essentially object — you can do almost nothing with it. Constraints unlock members, operators, and constructors that make generic code genuinely useful.
The Basic Constraints
C# provides several constraint categories, applied with the where clause:
// Must be a reference type
public class Cache<T> where T : class { }
// Must be a non-nullable value type
public struct Wrapper<T> where T : struct { }
// Must have a parameterless constructor
public T CreateInstance<T>() where T : new() => new T();
// Must not be null (reference or value type, but not nullable)
public void Process<T>(T value) where T : notnull { }
These constraints control the fundamental nature of the type parameter.
Interface and Base Class Constraints
The most common constraints require a type to implement an interface or derive from a base class:
public class Repository<T> where T : IEntity
{
public void Save(T entity)
{
// We can access IEntity members because of the constraint
Console.WriteLine($"Saving entity with ID: {entity.Id}");
}
}
public interface IEntity
{
int Id { get; }
}
You can combine multiple interface constraints:
public class SortedFilteredCollection<T>
where T : IComparable<T>, IEquatable<T>
{
private readonly List<T> _items = [];
public void Add(T item) { /* ... */ }
public T? Find(T target) =>
_items.FirstOrDefault(x => x.Equals(target));
}
The unmanaged Constraint
The unmanaged constraint restricts a type to unmanaged types — value types that contain no references:
public unsafe void WriteToBuffer<T>(T value, byte* destination) where T : unmanaged
{
int size = sizeof(T); // Only works with unmanaged constraint
Buffer.MemoryCopy(&value, destination, size, size);
}
This is essential for interop scenarios and high-performance memory manipulation.
Combining Constraints
Constraints can be combined, but some combinations are mutually exclusive:
public class EventStore<TEvent>
where TEvent : class, IEvent, new()
{
public TEvent CreateDefault()
{
var evt = new TEvent(); // Requires new()
evt.Timestamp = DateTime.UtcNow; // Requires IEvent
return evt;
}
}
You cannot combine struct with class, or struct with new() (structs always have a parameterless constructor).
Generic Constraints on Multiple Parameters
When a method or class has multiple type parameters, each can have independent constraints:
public class Mapper<TSource, TDestination>
where TSource : class
where TDestination : class, new()
{
private readonly Func<TSource, TDestination> _mapFunc;
public Mapper(Func<TSource, TDestination> mapFunc)
{
_mapFunc = mapFunc;
}
public TDestination Map(TSource source) => _mapFunc(source);
}
Type Relationships
You can constrain one type parameter to be related to another:
public class ComparableCollection<T> where T : IComparable<T>
{
private readonly List<T> _items = [];
public T Max() => _items.Max()!;
}
// Constrain TDerived to be a subtype of TBase
public TDerived Convert<TBase, TDerived>(TBase item)
where TDerived : TBase
{
return (TDerived)item!;
}
Static Abstract Interface Members (C# 11+)
With static abstract members in interfaces, constraints unlock operator usage and factory patterns:
public T Add<T>(T left, T right) where T : INumber<T>
{
return left + right; // Works because INumber<T> defines operator +
}
public T Parse<T>(string input) where T : IParsable<T>
{
return T.Parse(input, null); // Static method called on the type parameter
}
This is a significant expansion of what generic constraints can express, enabling truly generic mathematical code.
Real-World Example: A Generic Validator
public interface IValidatable
{
IEnumerable<string> Validate();
}
public class ValidationPipeline<T> where T : IValidatable
{
private readonly List<Func<T, IEnumerable<string>>> _rules = [];
public ValidationPipeline<T> AddRule(Func<T, IEnumerable<string>> rule)
{
_rules.Add(rule);
return this;
}
public ValidationResult Validate(T entity)
{
var errors = entity.Validate()
.Concat(_rules.SelectMany(rule => rule(entity)))
.ToList();
return errors.Count == 0
? ValidationResult.Success()
: ValidationResult.Failure(errors);
}
}
Choosing the Right Constraints
Use constraints liberally — they make your generic code safer and more useful:
- Use
classorstructwhen your implementation depends on reference or value semantics - Use interface constraints to access specific members
- Use
new()when you need to create instances - Use
notnullwhen null is not a valid type argument - Use
unmanagedfor interop and binary operations
Every constraint you add is a contract that callers must satisfy and that your implementation can rely on. They turn generic code from a loose template into a precise specification.