Without constraints, a generic type parameter T is treated as object inside the generic body — you can only call methods that exist on every type (ToString(), GetHashCode(), Equals()). Constraints (where T : something) tell the compiler more about what T can be, unlocking the methods and properties of the constraint type inside the generic body. They also protect callers from passing inappropriate types. Constraints are essential for building the generic repository pattern used throughout the Web API part of this series.
Common Constraints
// ── where T : class — T must be a reference type ──────────────────────────
public class Cache<T> where T : class
{
private T? _value;
public void Set(T value) => _value = value;
public T? Get() => _value; // can return null because T is a reference type
}
// ── where T : struct — T must be a value type ─────────────────────────────
public static T? ParseNullable<T>(string? input, TryParseDelegate<T> tryParse)
where T : struct
{
if (input is null) return null;
return tryParse(input, out T result) ? result : null;
}
delegate bool TryParseDelegate<T>(string s, out T result);
// Usage:
int? age = ParseNullable<int>("25", int.TryParse);
double? rate = ParseNullable<double>("3.14", double.TryParse);
// ── where T : new() — T must have a parameterless constructor ─────────────
public T CreateDefault<T>() where T : new() => new T(); // ✓ can call new T()
// ── where T : SomeInterface — T must implement the interface ─────────────
public static void SaveAll<T>(IEnumerable<T> items) where T : IAuditable
{
foreach (var item in items)
{
item.AuditedAt = DateTime.UtcNow; // ✓ IAuditable.AuditedAt is accessible
Save(item);
}
}
// ── where T : BaseEntity — T must inherit from BaseEntity ─────────────────
public class Repository<T> where T : BaseEntity
{
public T? GetById(int id) { /* ... */ return default; }
public void SoftDelete(T entity)
{
entity.IsDeleted = true; // ✓ BaseEntity.IsDeleted is accessible
entity.DeletedAt = DateTime.UtcNow;
}
}
// ── where T : notnull — T cannot be null (value or non-nullable reference) ─
public class NonNullCache<T> where T : notnull
{
private readonly Dictionary<string, T> _store = new();
public T Get(string key) => _store[key]; // never returns null
}
Cache<int> when Cache<T> requires where T : class, you get a compile error, not a runtime error. The where T : class constraint also changes the nullability of T? inside the body — without it, the compiler does not know whether T is a reference type (where T? means nullable reference) or a value type (where T? means Nullable<T>).where clauses: where T : class, IAuditable, new(). This means T must be a reference type, implement IAuditable, and have a parameterless constructor — all three must hold. When building generic repository base classes, the typical constraint is where T : class (it’s a reference type) combined with where T : IEntity (it has an Id property) to enable operations like GetByIdAsync(int id).Multiple Type Parameters with Constraints
// Multiple type parameters, each with independent constraints
public class Mapper<TSource, TDestination>
where TSource : class
where TDestination : class, new() // must be ref type AND have parameterless ctor
{
private readonly Func<TSource, TDestination> _mapFn;
public Mapper(Func<TSource, TDestination> mapFn) => _mapFn = mapFn;
public TDestination Map(TSource source)
{
ArgumentNullException.ThrowIfNull(source);
return _mapFn(source);
}
public IReadOnlyList<TDestination> MapAll(IEnumerable<TSource> sources)
=> sources.Select(Map).ToList();
}
// Usage
var mapper = new Mapper<Post, PostDto>(
post => new PostDto { Id = post.Id, Title = post.Title }
);
PostDto dto = mapper.Map(post);
Common Mistakes
Mistake 1 — Calling T-specific methods without a constraint
❌ Wrong — compile error: T has no Id property without a constraint declaring it:
public class Repo<T>
{
public T? GetById(int id)
=> _items.FirstOrDefault(x => x.Id == id); // compile error — T has no .Id!
}
✅ Correct — constrain T to an interface that declares Id:
public class Repo<T> where T : IEntity
{
public T? GetById(int id)
=> _items.FirstOrDefault(x => x.Id == id); // ✓ IEntity declares Id
}
Mistake 2 — Over-constraining to a concrete class (defeats purpose of generics)
❌ Wrong — only Post can be T; generics add no value:
public class Repo<T> where T : Post { }
✅ Correct — constrain to the interface or base class that provides the needed members.