Generic constraints allow us to restrict which types can be passed to a generic class, method, or interface.
They are defined using the where keyword and help ensure type safety at compile time.
1. What are generic constraints, and why do we use the where keyword?
What are generic constraints?
By default, a generic type parameter T can represent many different data types.
Generic constraints restrict T to specific categories of types, such as reference types, value types, interfaces, or base classes.
Syntax:
class ClassName<T> where T : constraint
{
}
Example without constraints
public class Repository<T>
{
public void Save(T item)
{
Console.WriteLine("Item saved");
}
}
var repo1 = new Repository<int>();
var repo2 = new Repository<string>();
Both int and string are accepted because there are no constraints.
Example with constraints
public class Repository<T> where T : class
{
public void Save(T item)
{
Console.WriteLine("Item saved");
}
}
var repo1 = new Repository<string>(); // Valid
var repo2 = new Repository<int>(); // Compile-time error
Explanation:
-
where T : classrestrictsTto reference types. -
stringis a reference type, so it is accepted. -
intis a value type, so it is rejected. -
Invalid types are identified during compilation.
Diagram
Repository<T>
|
| where T : class
|
+---- string -> Allowed
|
+---- Employee -> Allowed
|
+---- int -> Not Allowed
|
+---- bool -> Not Allowed
Key Points
-
wheredefines constraints on generic type parameters. -
Constraints provide compile-time type safety.
-
They allow generic code to use members guaranteed by the constraint.
-
They help prevent invalid type usage.
-
Commonly used in generic repositories, services, and utility methods.
2. What is the difference between where T : class and where T : struct?
Both are generic constraints, but they restrict different categories of data types.
Comparison
|
Feature |
|
|
|---|---|---|
|
Accepts |
Reference types |
Non-nullable value types |
|
|
Allowed |
Not allowed |
|
|
Not allowed |
Allowed |
|
|
Allowed |
Not allowed |
|
|
Not allowed |
Allowed |
|
Nullable value type |
Not allowed |
Not allowed |
|
Default value |
Usually |
Zero-initialized value |
Example Program
public class ReferenceExample<T> where T : class
{
public void Display(T value)
{
Console.WriteLine(value);
}
}
public class ValueExample<T> where T : struct
{
public void Display(T value)
{
Console.WriteLine(value);
}
}
class Program
{
static void Main()
{
var refExample = new ReferenceExample<string>();
refExample.Display("Hello");
var valueExample = new ValueExample<int>();
valueExample.Display(100);
}
}
Output:
Hello
100
Explanation
where T : class
ReferenceExample<string> obj1; // Valid
ReferenceExample<int> obj2; // Error
Only reference types are accepted.
where T : struct
ValueExample<int> obj1; // Valid
ValueExample<double> obj2; // Valid
ValueExample<string> obj3; // Error
Only non-nullable value types are accepted.
Diagram
Generic Type T
|
+--------+--------+
| |
where T : class where T : struct
| |
Reference Types Value Types
| |
string int
Employee double
Customer DateTime
Key Points
-
classrestricts types to reference types. -
structrestricts types to non-nullable value types. -
where T : classpermits nullable references unless nullable reference type rules specify otherwise;class?explicitly permits nullable reference types. -
structcannot acceptNullable<T>such asint?. -
Both constraints are checked at compile time.
3. What is the purpose of where T : new()?
What is it?
The new() constraint ensures that the generic type has a public parameterless constructor.
It allows us to create an instance of T inside a generic class or method.
Syntax
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
Without new(), the compiler generally cannot guarantee that new T() is valid.
Example Program
public class Employee
{
public string Name { get; set; } = "John";
public Employee()
{
}
}
public class ObjectFactory<T> where T : new()
{
public T CreateObject()
{
return new T();
}
}
class Program
{
static void Main()
{
var factory = new ObjectFactory<Employee>();
Employee employee = factory.CreateObject();
Console.WriteLine(employee.Name);
}
}
Output:
John
Explanation
-
ObjectFactory<T>is a generic class. -
where T : new()requires an accessible public parameterless constructor for class types. -
Employeehas a public parameterless constructor. -
new T()creates anEmployeeobject. -
The newly created object's
Nameproperty contains"John".
Diagram
ObjectFactory<Employee>
|
| where T : new()
|
v
CreateObject()
|
v
new T()
|
v
new Employee()
|
v
Employee Object
Key Points
-
new()enables object creation usingnew T(). -
The type must be non-abstract and have a public parameterless constructor.
-
Value types also satisfy the
new()constraint. -
When combined with other constraints,
new()must appear last. -
Commonly used in generic factories and object creation utilities.
Important: A class with only a constructor requiring parameters cannot satisfy new().
public class Employee
{
public Employee(int id)
{
}
}
// Compile-time error
var factory = new ObjectFactory<Employee>();
4. How do you restrict a generic type to an interface or base class?
We can restrict a generic type parameter to a particular interface or base class using where T : InterfaceName or where T : BaseClassName.
This ensures the supplied type implements the required interface or inherits from the specified base class.
A. Interface Constraint
Syntax:
class Repository<T> where T : IEntity
{
}
Example Program
public interface IEntity
{
int Id { get; set; }
}
public class Employee : IEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Repository<T> where T : IEntity
{
public void Save(T entity)
{
Console.WriteLine($"Saving entity ID: {entity.Id}");
}
}
class Program
{
static void Main()
{
var repo = new Repository<Employee>();
repo.Save(new Employee
{
Id = 101,
Name = "John"
});
}
}
Output:
Saving entity ID: 101
Explanation
-
IEntitydefines anIdproperty. -
EmployeeimplementsIEntity. -
Repository<T>accepts only types implementingIEntity. -
Because of the constraint, the repository can safely access
entity.Id.
Diagram
IEntity
|
Defines Id
|
v
Employee
|
Implements IEntity
|
v
Repository<Employee>
|
v
Save(Employee)
B. Base Class Constraint
We can also restrict generic types to a particular base class.
Example
public class Person
{
public string Name { get; set; } = "";
}
public class Employee : Person
{
public int EmployeeId { get; set; }
}
public class Service<T> where T : Person
{
public void Display(T person)
{
Console.WriteLine(person.Name);
}
}
class Program
{
static void Main()
{
var service = new Service<Employee>();
service.Display(new Employee
{
Name = "John",
EmployeeId = 101
});
}
}
Output:
John
Explanation
-
Personis the base class. -
Employeeinherits fromPerson. -
where T : PersonrestrictsTtoPersonor derived types. -
Service<T>can access members defined inPerson.
Comparison
|
Interface Constraint |
Base Class Constraint |
|---|---|
|
|
|
|
Requires interface implementation |
Requires inheritance |
|
Supports unrelated implementing classes |
Supports one inheritance hierarchy |
|
Useful for contracts |
Useful for shared base functionality |
Key Points
-
Interface constraints enforce a contract.
-
Base class constraints enforce inheritance.
-
Both allow access to members guaranteed by the constraint.
-
Interface constraints are commonly used in generic repositories.
-
Base class constraints are useful when entities share a common base class.
5. Can we apply multiple constraints to a generic type?
Yes. C# supports multiple constraints on the same generic type parameter.
This allows us to enforce several requirements simultaneously.
Syntax
public class Repository<T>
where T : class, IEntity, new()
{
}
Here:
-
class—Tmust be a reference type. -
IEntity—Tmust implementIEntity. -
new()—Tmust have a public parameterless constructor.
Example Program
public interface IEntity
{
int Id { get; set; }
}
public class Employee : IEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Repository<T>
where T : class, IEntity, new()
{
public T Create(int id)
{
T entity = new T();
entity.Id = id;
return entity;
}
}
class Program
{
static void Main()
{
var repo = new Repository<Employee>();
Employee employee = repo.Create(101);
Console.WriteLine(employee.Id);
}
}
Output:
101
Explanation
-
Employeeis a class, satisfyingclass. -
EmployeeimplementsIEntity, satisfying the interface constraint. -
Employeehas an implicit public parameterless constructor, satisfyingnew(). -
new T()creates the employee object. -
The repository assigns the ID and returns the object.
Diagram
Repository<T>
|
v
Multiple Constraints
|
+--------+---------+
| | |
class IEntity new()
| | |
v v v
Reference Implements Public
Type Interface Default
Constructor
| | |
+--------+---------+
|
v
Employee Accepted
Rules for Multiple Constraints
The order of constraints is important.
// Valid
where T : class, IEntity, new()
// Valid
where T : Person, IEntity, new()
// Valid
where T : IEntity, new()
// Invalid: new() must be last
where T : new(), IEntity
// Invalid: class and struct cannot be combined
where T : class, struct
Multiple generic type parameters can also have different constraints.
public class Mapper<TSource, TDestination>
where TSource : class
where TDestination : class, new()
{
public TDestination CreateDestination()
{
return new TDestination();
}
}
Here, TSource must be a reference type, while TDestination must be a reference type with a public parameterless constructor.
Key Points
-
Multiple constraints can be combined.
-
The
class,struct,notnull, orunmanagedconstraint, when used, must appear first. -
A base class constraint must appear before interface constraints.
-
new()must be last when used. -
Each generic type parameter can have its own
whereclause. -
Multiple constraints improve type safety and enable more specialized generic implementations.
Quick Revision – Generic Constraints
|
Constraint |
Purpose |
Example |
|---|---|---|
|
|
Reference types only |
|
|
|
Non-nullable value types only |
|
|
|
Public parameterless constructor |
|
|
|
Must implement interface |
|
|
|
Must inherit from base class or be that class |
|
|
|
Non-nullable type argument |
|
|
|
Unmanaged value type |
|
|
|
Multiple requirements |
Generic repository |
Real-Time Scenario: Generic Repository in ASP.NET Core
Generic constraints are particularly useful when building reusable repository classes.
public interface IEntity
{
int Id { get; set; }
}
public class Repository<T>
where T : class, IEntity
{
private readonly DbContext _context;
public Repository(DbContext context)
{
_context = context;
}
public async Task<T?> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
}
}
Why are constraints useful here?
-
classensuresTis a reference type, as required by EF Core'sDbContext.Set<TEntity>(). -
IEntityensures all repository entities expose anId. -
The same repository can be reused for
Employee,Customer,Product, and other implementing entity classes. -
The repository cannot accept types that violate its constraints.
Interview takeaway: Generic constraints make reusable code more type-safe by specifying exactly which types are permitted and which operations can safely be performed on them.