Overview
Generic type constraints in C# define what kind of type can be used as a generic type argument. They are written with the where keyword and allow a generic class, method, interface, delegate, or struct to safely use members or behaviors that are guaranteed to exist on the type parameter.
Generics help developers write reusable, type-safe components without duplicating code for every concrete type. Constraints make those reusable components more precise. Instead of accepting "any type", a generic component can require a reference type, value type, interface implementation, base class, public parameterless constructor, enum, delegate, unmanaged type, or another generic type relationship.
This topic matters because generics appear everywhere in real C# and .NET development:
List<T>,Dictionary<TKey, TValue>,IEnumerable<T>, and LINQ- Repository and service abstractions
- CQRS commands, queries, handlers, validators, and pipeline behaviors
- Result wrappers such as
Result<T> - Mapping, caching, serialization, and factory components
- Generic math and reusable algorithms
- Strongly typed IDs and domain models
For interviews, generic constraints are important because they test whether a developer understands type safety, reusable design, compile-time guarantees, inheritance, interfaces, variance, nullable reference types, and the trade-offs between abstraction and complexity. A strong candidate should know not only the syntax, but also when constraints improve a design and when they make code unnecessarily complicated.
Core Concepts
What Generics Solve
Generics allow code to work with a type parameter instead of a specific type.
Without generics, reusable code often uses object, which loses type safety and can require casts:
public class ObjectBox
{
public object Value { get; set; }
public ObjectBox(object value)
{
Value = value;
}
}
var box = new ObjectBox("hello");
string text = (string)box.Value;
With generics, the type is known at compile time:
public class Box<T>
{
public T Value { get; }
public Box(T value)
{
Value = value;
}
}
var box = new Box<string>("hello");
string text = box.Value;
Benefits:
- Compile-time type safety
- Less casting
- Better readability
- Better reuse
- Better performance for value types because generic collections avoid many boxing scenarios
- Clearer APIs because the expected type is part of the method or class signature
Common interview point: generics are not just about avoiding duplicate code. They also express intent and let the compiler catch errors earlier.
What Type Constraints Are
A type constraint tells the compiler what a type parameter must support.
public static T Max<T>(T left, T right)
where T : IComparable<T>
{
return left.CompareTo(right) >= 0 ? left : right;
}
Without where T : IComparable<T>, the compiler cannot allow left.CompareTo(right) because it cannot assume every possible T has a CompareTo method.
A constraint acts like a contract between the reusable component and the caller:
- The component promises to work for any type that satisfies the constraint.
- The caller must provide a type that satisfies the constraint.
- The compiler validates the rule before runtime.
Basic Generic Syntax
Generic classes:
public class Repository<TEntity>
{
private readonly List<TEntity> _items = new();
public void Add(TEntity entity)
{
_items.Add(entity);
}
public IReadOnlyList<TEntity> GetAll()
{
return _items;
}
}
Generic methods:
public static T FirstOrDefaultValue<T>(IEnumerable<T> items, T fallback)
{
foreach (var item in items)
{
return item;
}
return fallback;
}
Generic interfaces:
public interface IHandler<TRequest, TResponse>
{
Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);
}
Generic delegates:
public delegate TResult Converter<in TInput, out TResult>(TInput input);
In real applications, generic interfaces are common in CQRS, validation, mapping, pipelines, caching, and event handling.
The where Keyword
Constraints are declared using where clauses.
public class EfRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
where TKey : notnull
{
public Task<TEntity?> FindAsync(TKey id)
{
throw new NotImplementedException();
}
}
This means:
TEntitymust be a non-nullable reference type.TEntitymust implementIEntity<TKey>.TKeymust be non-nullable.
Multiple type parameters can have separate constraints.
public interface IMapper<TSource, TDestination>
where TSource : class
where TDestination : class, new()
{
TDestination Map(TSource source);
}
Common Constraint Types
C# supports several important constraint forms.
where T : class
Requires T to be a reference type.
public class ReferenceCache<T>
where T : class
{
private readonly Dictionary<string, T> _cache = new();
public T? Get(string key)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}
In a nullable-enabled project, where T : class means T is a non-nullable reference type. Use class? when nullable reference type arguments are allowed.
where T : class?
Allows nullable or non-nullable reference types.
public static bool IsMissing<T>(T value)
where T : class?
{
return value is null;
}
This is useful when the component intentionally accepts nullable reference values.
where T : struct
Requires T to be a non-nullable value type.
public static bool IsDefault<T>(T value)
where T : struct
{
return EqualityComparer<T>.Default.Equals(value, default);
}
Important details:
structexcludes nullable value types such asint?.structimplies an accessible parameterless constructor.structcannot be combined withclass,class?,notnull,unmanaged, ornew().
where T : notnull
Requires T to be a non-nullable type.
public class Lookup<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _items = new();
public void Add(TKey key, TValue value)
{
_items.Add(key, value);
}
}
This is common for dictionary keys, cache keys, IDs, and strongly typed lookup structures.
Important interview detail: notnull works with nullable reference type analysis. Violations normally produce compiler warnings rather than hard errors.
where T : unmanaged
Requires T to be an unmanaged value type.
public static int SizeOf<T>()
where T : unmanaged
{
return System.Runtime.InteropServices.Marshal.SizeOf<T>();
}
This is useful for low-level code, interop, binary serialization, memory operations, and performance-sensitive algorithms.
Important details:
unmanagedimpliesstruct.unmanagedcannot be combined withstructornew().- It is not commonly needed in normal business applications.
where T : new()
Requires a public parameterless constructor.
public static T Create<T>()
where T : new()
{
return new T();
}
This is useful for simple factories, mapping components, test helpers, and object initialization.
Important details:
new()must appear last when combined with other constraints.- It only allows public parameterless construction.
- It does not work for constructors with parameters.
- Overusing
new()can lead to weak object creation because dependencies and invariants may be bypassed.
Base Class Constraint
Requires T to inherit from a specific base class.
public abstract class Entity
{
public Guid Id { get; init; }
}
public class AuditService<TEntity>
where TEntity : Entity
{
public string GetAuditKey(TEntity entity)
{
return $"{typeof(TEntity).Name}:{entity.Id}";
}
}
Base class constraints are useful when a reusable component needs shared behavior or state from a base class.
Trade-off: base class constraints couple the generic component to a specific inheritance hierarchy. Interface constraints are often more flexible.
Interface Constraint
Requires T to implement an interface.
public interface IEntity<TKey>
{
TKey Id { get; }
}
public class Repository<TEntity, TKey>
where TEntity : IEntity<TKey>
where TKey : notnull
{
public TKey GetId(TEntity entity)
{
return entity.Id;
}
}
Interface constraints are one of the most common and useful constraint types because they express behavior without forcing inheritance from a base class.
Common examples:
where T : IDisposable
where T : IComparable<T>
where T : IEquatable<T>
where T : IEntity<Guid>
where T : ICommand<TResult>
where T : IValidator<TRequest>
Multiple Constraints
A type parameter can have multiple constraints.
public class CsvExporter<T>
where T : class, IExportable, new()
{
public string ExportNewItem()
{
var item = new T();
return item.ToCsv();
}
}
public interface IExportable
{
string ToCsv();
}
Constraint ordering matters:
// Correct
where T : class, IExportable, new()
// Incorrect
// where T : new(), class, IExportable
General order:
- Primary kind constraint such as
class,class?,struct,notnull, orunmanaged - Base class constraint, if used
- Interface constraints
new()constraint last- Anti-constraints such as
allows ref struct, when applicable
Enum Constraints
C# supports constraining a type parameter to System.Enum.
public static IReadOnlyList<TEnum> GetEnumValues<TEnum>()
where TEnum : struct, Enum
{
return Enum.GetValues<TEnum>();
}
This is useful for reusable enum helpers.
public enum OrderStatus
{
Draft,
Submitted,
Paid,
Cancelled
}
var values = GetEnumValues<OrderStatus>();
Why this is better than accepting Enum directly:
- Stronger type safety
- No need to pass
typeof(OrderStatus)manually - Easier to build reusable helpers
- Better caller experience
Delegate Constraints
A type can be constrained to Delegate or MulticastDelegate.
public static TDelegate Combine<TDelegate>(TDelegate first, TDelegate second)
where TDelegate : Delegate
{
return (TDelegate)Delegate.Combine(first, second);
}
This is useful for reusable delegate utilities, callback composition, and event-related infrastructure.
In everyday business code, delegate constraints are less common than interface and base class constraints, but they are useful to know for advanced interviews.
Type Parameter as a Constraint
One type parameter can constrain another.
public static void CopyTo<TSource, TDestination>(
IEnumerable<TSource> source,
ICollection<TDestination> destination)
where TSource : TDestination
{
foreach (var item in source)
{
destination.Add(item);
}
}
This means every TSource must be assignable to TDestination.
List<string> strings = ["a", "b"];
List<object> objects = [];
CopyTo<string, object>(strings, objects);
This is useful when modeling relationships between type parameters.
Generic Methods vs Generic Classes
A generic class makes the whole type reusable for a type parameter.
public class PagedResult<T>
{
public IReadOnlyList<T> Items { get; init; } = [];
public int TotalCount { get; init; }
}
A generic method makes only one operation generic.
public static PagedResult<T> ToPagedResult<T>(
IReadOnlyList<T> items,
int totalCount)
{
return new PagedResult<T>
{
Items = items,
TotalCount = totalCount
};
}
Use a generic class when the type parameter is part of the object's state or identity.
Use a generic method when the type parameter is only needed for one operation.
Common mistake: making an entire class generic when only one method needs the type parameter.
Constraints and Nullable Reference Types
Generic constraints interact with nullable reference types.
public class RequiredValue<T>
where T : notnull
{
public T Value { get; }
public RequiredValue(T value)
{
Value = value;
}
}
This prevents nullable values from being used as valid type arguments in nullable-aware code.
Compare these constraints:
where T : class // non-nullable reference type in nullable context
where T : class? // nullable or non-nullable reference type
where T : notnull // non-nullable reference type or non-nullable value type
where T : struct // non-nullable value type
Important habit: choose the constraint that matches the real domain rule.
public class Cache<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _cache = new();
public TValue? GetOrDefault(TKey key)
{
return _cache.TryGetValue(key, out var value) ? value : default;
}
}
TKey should be notnull because dictionary keys should not be null. TValue may or may not be nullable depending on the business case.
Generic Constraints and Reusable Domain Components
Generics are common in domain and application layers.
public interface IEntity<TKey>
{
TKey Id { get; }
}
public interface IRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
where TKey : notnull
{
Task<TEntity?> FindByIdAsync(TKey id, CancellationToken cancellationToken);
Task AddAsync(TEntity entity, CancellationToken cancellationToken);
}
public sealed class Product : IEntity<Guid>
{
public Guid Id { get; init; }
public string Name { get; set; } = string.Empty;
}
This design makes the repository reusable for many entity types while still guaranteeing that each entity has an ID.
Interview trade-off:
- Good: type-safe, reusable, consistent abstraction
- Bad: can become too generic and hide important persistence details
- Bad: generic repositories may not fit complex aggregate-specific queries
- Best practice: use generic abstractions where behavior is truly common, and use specific repositories or query services for domain-specific operations
Generic Constraints in CQRS and MediatR-Style Designs
CQRS patterns often use generics for commands, queries, handlers, and pipeline behaviors.
public interface ICommand<TResult>
{
}
public interface ICommandHandler<TCommand, TResult>
where TCommand : ICommand<TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
}
public sealed record CreateProductCommand(string Name) : ICommand<Guid>;
public sealed class CreateProductHandler
: ICommandHandler<CreateProductCommand, Guid>
{
public Task<Guid> HandleAsync(
CreateProductCommand command,
CancellationToken cancellationToken)
{
var productId = Guid.NewGuid();
return Task.FromResult(productId);
}
}
The constraint ensures that a handler can only be created for a request type that actually represents a command returning the expected result.
Generic Constraints for Validation
Reusable validation pipelines often use generic constraints.
public interface IValidator<T>
{
IReadOnlyList<string> Validate(T instance);
}
public sealed class ValidationBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public IReadOnlyList<string> Validate(TRequest request)
{
return _validators
.SelectMany(validator => validator.Validate(request))
.ToList();
}
}
This style is common in production .NET applications because it allows cross-cutting behavior to be implemented once and reused for many request types.
Generic Constraints for Factories
The new() constraint can be used for simple factories.
public interface IHasCreatedAt
{
DateTime CreatedAtUtc { get; set; }
}
public static class Factory
{
public static T Create<T>()
where T : IHasCreatedAt, new()
{
return new T
{
CreatedAtUtc = DateTime.UtcNow
};
}
}
However, in real applications, constructors often require dependencies or required values.
Better for domain objects:
public sealed class Order
{
public Guid Id { get; }
public string CustomerNumber { get; }
public Order(Guid id, string customerNumber)
{
Id = id;
CustomerNumber = string.IsNullOrWhiteSpace(customerNumber)
? throw new ArgumentException("Customer number is required.", nameof(customerNumber))
: customerNumber;
}
}
Avoid using new() if it forces domain objects to have weak parameterless constructors.
Generic Math and Static Abstract Interface Members
Modern C# supports static abstract members in interfaces, which enables generic math and reusable numeric algorithms.
public static T Add<T>(T left, T right)
where T : System.Numerics.IAdditionOperators<T, T, T>
{
return left + right;
}
Before static abstract interface members, writing reusable numeric code was difficult because operators like + could not be expressed through normal interface constraints.
public static T Sum<T>(IEnumerable<T> values)
where T :
System.Numerics.IAdditionOperators<T, T, T>,
System.Numerics.IAdditiveIdentity<T, T>
{
var total = T.AdditiveIdentity;
foreach (var value in values)
{
total += value;
}
return total;
}
This is useful for numeric algorithms, financial calculations, scientific code, statistics helpers, and reusable math libraries.
Interview point: this is an advanced feature. Most business applications do not need it, but it shows how constraints can describe compile-time capabilities beyond normal instance methods.
Variance in Generic Interfaces
Variance controls how generic interface types can be assigned when their type arguments have inheritance relationships.
Covariance uses out and is for producer/output positions.
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
This works because IEnumerable<out T> only produces values of T.
Contravariance uses in and is for consumer/input positions.
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer;
This works because an IComparer<object> can compare strings, since strings are objects.
Custom examples:
public interface IProducer<out T>
{
T Produce();
}
public interface IConsumer<in T>
{
void Consume(T item);
}
Rules:
- Use
out Twhen the interface returnsTbut does not acceptTas input. - Use
in Twhen the interface acceptsTas input but does not returnT. - Value types do not support variance conversions.
- Classes are invariant even if they implement variant interfaces.
Common interview trap:
List<string> strings = new();
IEnumerable<object> objects = strings; // Valid
// List<object> objectList = strings; // Invalid
List<T> is invariant because it both consumes and produces T.
Open Generic Types and Dependency Injection
In ASP.NET Core and other .NET applications, open generic registrations allow one implementation to be reused for many closed generic types.
services.AddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));
This means the container can resolve:
IRepository<Product, Guid>
IRepository<Customer, int>
IRepository<Order, Guid>
A common validation pipeline example:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
This is powerful because reusable generic components can be wired once and applied across many application types.
Interview point: open generics are often used for repositories, validators, handlers, decorators, pipeline behaviors, mappers, and caching wrappers.
Runtime Behavior and Performance
Generics are checked at compile time, but they also have runtime behavior.
Important practical points:
- Generic code avoids many casts that would be needed with
object. - Generic collections such as
List<int>avoid boxing eachint. - Reflection-based generic creation is more flexible but slower and less type-safe.
- Generic constraints let the compiler call constrained members directly.
- Overly abstract generic designs can make debugging and stack traces harder to understand.
Example of avoiding boxing:
var numbers = new List<int>();
numbers.Add(10); // No boxing into object
Compared with:
var numbers = new ArrayList();
numbers.Add(10); // int is boxed as object
In modern C#, prefer generic collections and generic interfaces over non-generic collections.
Practical Design Examples
Reusable Result Type
public sealed class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
private Result(bool isSuccess, T? value, string? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result<T> Success(T value)
{
return new Result<T>(true, value, null);
}
public static Result<T> Failure(string error)
{
return new Result<T>(false, default, error);
}
}
This generic type is reusable for many response values.
Result<Guid> result = Result<Guid>.Success(Guid.NewGuid());
Result<string> failed = Result<string>.Failure("Name is required.");
Strongly Typed ID Pattern
public interface IStronglyTypedId<TValue>
where TValue : notnull
{
TValue Value { get; }
}
public readonly record struct ProductId(Guid Value)
: IStronglyTypedId<Guid>;
public readonly record struct CustomerId(Guid Value)
: IStronglyTypedId<Guid>;
This avoids mixing unrelated IDs accidentally.
public sealed class Product
{
public ProductId Id { get; init; }
}
Generic constraints can then work with typed IDs:
public static string FormatId<TId, TValue>(TId id)
where TId : IStronglyTypedId<TValue>
where TValue : notnull
{
return id.Value.ToString()!;
}
Generic Cache Wrapper
public interface ICache<TKey, TValue>
where TKey : notnull
{
bool TryGet(TKey key, out TValue value);
void Set(TKey key, TValue value);
}
public sealed class MemoryCache<TKey, TValue> : ICache<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _items = new();
public bool TryGet(TKey key, out TValue value)
{
return _items.TryGetValue(key, out value!);
}
public void Set(TKey key, TValue value)
{
_items[key] = value;
}
}
The notnull constraint clearly expresses that cache keys cannot be null.
Generic Comparable Helper
public static class SortHelper
{
public static IReadOnlyList<T> Sort<T>(IEnumerable<T> items)
where T : IComparable<T>
{
return items.OrderBy(item => item).ToList();
}
}
The constraint allows sorting only for types that define comparison logic.
Constraints vs Runtime Checks
A constraint is checked at compile time.
public static void DisposeItem<T>(T item)
where T : IDisposable
{
item.Dispose();
}
A runtime check is checked while the program runs.
public static void DisposeIfPossible<T>(T item)
{
if (item is IDisposable disposable)
{
disposable.Dispose();
}
}
Use constraints when the operation requires a capability.
Use runtime checks when the capability is optional.
Interview comparison:
Constraints vs Inheritance vs Interfaces
Generic constraints can use both base classes and interfaces, but the design intent is different.
Base class constraint:
where T : Entity
Use when all types truly share a common base implementation.
Interface constraint:
where T : IEntity<Guid>
Use when the component only needs a behavior or contract.
Generic interface without base class:
public interface IAuditable
{
DateTime CreatedAtUtc { get; }
}
public static DateTime GetCreatedAt<T>(T item)
where T : IAuditable
{
return item.CreatedAtUtc;
}
In most reusable business components, interface constraints are more flexible than base class constraints.
Common Mistakes
Overusing Generics
Not every abstraction needs generics.
public class ProductService<TProduct>
{
// Only ever used with Product
}
If only one type is valid, use the concrete type.
Over-Constraining Type Parameters
public class ReportBuilder<T>
where T : class, new()
{
}
If the class never creates T, the new() constraint is unnecessary.
Only add constraints that the implementation actually needs.
Using new() for Domain Entities
public class EntityFactory<T>
where T : new()
{
public T Create() => new T();
}
This can force domain entities to expose parameterless constructors and allow invalid objects.
Prefer explicit factories for domain objects with required data.
Forgetting notnull for Keys
public class Cache<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> _items = new();
}
Better:
public class Cache<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _items = new();
}
Keys should usually be non-null.
Assuming default(T) Is Always Safe
public static T GetDefault<T>()
{
return default!;
}
default(T) can be:
0forintfalseforboolnullfor reference types- A zero-initialized struct
- A value that may be invalid for the domain
Avoid using default as a fake valid value unless the behavior is clearly documented.
Confusing class, class?, and notnull
where T : class
This means non-nullable reference type in nullable-aware code.
where T : class?
This allows nullable reference types.
where T : notnull
This allows non-nullable reference types and non-nullable value types.
Choose based on the real nullability rule.
Assuming IEnumerable<T> Allows Modification
public void AddItem<T>(IEnumerable<T> items, T item)
{
// Cannot add to IEnumerable<T>
}
IEnumerable<T> is for enumeration. Use ICollection<T> or IList<T> if mutation is required.
Returning Overly Generic Types
public object GetValue<T>()
{
return default(T)!;
}
Better:
public T? GetValue<T>()
{
return default;
}
If the method is generic, preserve the generic type information.
Best Practices
Use generics when the logic is genuinely reusable across multiple types.
Prefer interface constraints for behavior-based reuse:
where T : IAuditable
Use base class constraints only when shared implementation is truly required:
where T : Entity
Use notnull for dictionary keys, cache keys, IDs, and lookup types.
Avoid adding constraints before the implementation actually needs them.
Use nullable-aware constraints such as class, class?, and notnull intentionally.
Prefer generic collections such as List<T>, Dictionary<TKey, TValue>, and IReadOnlyList<T> over non-generic collections.
Avoid new() constraints for rich domain models that require valid constructor arguments.
Keep generic APIs readable. If a generic signature becomes hard to understand, the abstraction may be too broad.
Use descriptive type parameter names when the role is not obvious:
public interface IRepository<TEntity, TKey>
This is clearer than:
public interface IRepository<T, U>
Use common names such as T, TKey, TValue, TEntity, TRequest, TResponse, TCommand, and TQuery.
Prefer specific abstractions for domain-specific behavior. A generic repository should not become a dumping ground for every possible query.
Interview Mental Model
When choosing a generic constraint, ask:
- What operation does the generic component need to perform?
- Does every possible
Tsupport that operation? - Can an interface express the required behavior?
- Is a base class truly required?
- Should the type allow reference types, value types, nullable values, or only non-null values?
- Does the component need to create instances of
T? - Would a non-generic abstraction be simpler?
- Will this generic design remain understandable to other developers?
A good generic design is reusable but not vague. It should make invalid usage impossible or at least difficult.