← Back to Articles         
Generic Constraints

Generic Constraints

Published on 12 Oct 2026     9 min read C# Collections & Generics
Generic Constraints

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 : class restricts T to reference types.

  • string is a reference type, so it is accepted.

  • int is 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

  • where defines 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

where T : class

where T : struct

Accepts

Reference types

Non-nullable value types

string

Allowed

Not allowed

int

Not allowed

Allowed

Employee class

Allowed

Not allowed

DateTime

Not allowed

Allowed

Nullable value type int?

Not allowed

Not allowed

Default value

Usually null

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

  • class restricts types to reference types.

  • struct restricts types to non-nullable value types.

  • where T : class permits nullable references unless nullable reference type rules specify otherwise; class? explicitly permits nullable reference types.

  • struct cannot accept Nullable<T> such as int?.

  • 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

  1. ObjectFactory<T> is a generic class.

  2. where T : new() requires an accessible public parameterless constructor for class types.

  3. Employee has a public parameterless constructor.

  4. new T() creates an Employee object.

  5. The newly created object's Name property 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 using new 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

  • IEntity defines an Id property.

  • Employee implements IEntity.

  • Repository<T> accepts only types implementing IEntity.

  • 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

  • Person is the base class.

  • Employee inherits from Person.

  • where T : Person restricts T to Person or derived types.

  • Service<T> can access members defined in Person.

Comparison

Interface Constraint

Base Class Constraint

where T : IEntity

where T : Person

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 — T must be a reference type.

  • IEntity — T must implement IEntity.

  • new() — T must 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

  1. Employee is a class, satisfying class.

  2. Employee implements IEntity, satisfying the interface constraint.

  3. Employee has an implicit public parameterless constructor, satisfying new().

  4. new T() creates the employee object.

  5. 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, or unmanaged constraint, 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 where clause.

  • Multiple constraints improve type safety and enable more specialized generic implementations.

 

Quick Revision – Generic Constraints

Constraint

Purpose

Example

where T : class

Reference types only

string, Employee

where T : struct

Non-nullable value types only

int, DateTime

where T : new()

Public parameterless constructor

Employee

where T : IEntity

Must implement interface

Employee : IEntity

where T : Person

Must inherit from base class or be that class

Employee : Person

where T : notnull

Non-nullable type argument

int, string

where T : unmanaged

Unmanaged value type

int, double

where T : class, IEntity, new()

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?

  • class ensures T is a reference type, as required by EF Core's DbContext.Set<TEntity>().

  • IEntity ensures all repository entities expose an Id.

  • 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.

← Back to Articles