Introduction

Generic types in C# allow developers to write reusable and type-safe code that can work with different data types. However, there are situations where a generic class or method should only accept specific types.

For example, a generic method may need to create an object, access members of a particular base class, or call methods defined by an interface. Without restrictions, the compiler cannot guarantee that the supplied type supports those operations.

C# provides generic constraints to solve this problem. Generic constraints allow developers to specify requirements that a type argument must satisfy before it can be used with a generic class, method, or interface.

In this article, we will explore the commonly used generic constraints in C# and build practical examples using struct, class, new(), base-class, and interface constraints.

What Are Generic Constraints in C#?

A generic constraint is a rule applied to a type parameter that restricts which types can be used with a generic class or method.

Consider a simple generic class:

public class Repository<T>
{
    public void Add(T item)
    {
        Console.WriteLine($"Added: {item}");
    }
}

Without a constraint, T can represent many different types.

We can restrict T by using the where keyword:

public class Repository<T> where T : class
{
    public void Add(T item)
    {
        Console.WriteLine($"Added: {item}");
    }
}

Now, T must be a reference type.

The compiler enforces this restriction when the generic type is used.

Why Use Generic Constraints?

Generic constraints provide several benefits:

For example, if a generic method requires a type implementing IEmployee, adding an interface constraint guarantees that the required interface members are available inside the method.

Common Generic Constraints

C# provides several generic constraints.

Constraint

Purpose

where T : struct

Restricts T to non-nullable value types

where T : class

Restricts T to reference types

where T : new()

Requires a public parameterless constructor

where T : BaseClass

Restricts T to a base class or derived type

where T : IInterface

Restricts T to types implementing an interface

Let's look at each one with a practical example.

Using the struct Constraint

The struct constraint restricts the generic type parameter to a non-nullable value type.

Step 1: Create a Generic Method

public static void PrintValue<T>(T value) where T : struct
{
    Console.WriteLine($"Value: {value}");
}

The where T : struct constraint means that T must be a non-nullable value type.

Step 2: Use the Method

PrintValue(100);
PrintValue(25.5);

Both int and double are value types, so the calls are valid.

Output

Value: 100
Value: 25.5

Trying to use a reference type will produce a compile-time error:

PrintValue("Hello");

The compiler rejects this because string does not satisfy the struct constraint.

Using the class Constraint

The class constraint restricts the type parameter to a reference type.

Step 1: Create a Generic Method

public static void PrintObject<T>(T value) where T : class
{
    Console.WriteLine($"Object: {value}");
}

Step 2: Pass a Reference Type

string message = "Hello C#";

PrintObject(message);

Since string is a reference type, it satisfies the constraint.

Output

Object: Hello C#

A value type such as int cannot be used with this method:

PrintObject(100);

This results in a compile-time error because int is a value type.

Using the new() Constraint

The new() constraint requires the type argument to have a public parameterless constructor.

This is useful when generic code needs to create an instance of T.

Step 1: Create a Class

public class Employee
{
    public string Name { get; set; } = string.Empty;
}

Step 2: Create a Generic Method

public static T CreateInstance<T>() where T : new()
{
    return new T();
}

Because of the new() constraint, the compiler knows that T has a public parameterless constructor.

Step 3: Create an Employee

Employee employee = CreateInstance<Employee>();

employee.Name = "John";

Console.WriteLine(employee.Name);

Output

John

Without the new() constraint, the compiler would not allow the generic code to safely use new T().

Using a Base Class Constraint

A generic type can be restricted to a specific base class.

Step 1: Create a Base Class

public class Employee
{
    public string Name { get; set; } = string.Empty;

    public void DisplayName()
    {
        Console.WriteLine($"Employee: {Name}");
    }
}

Step 2: Create a Derived Class

public class Developer : Employee
{
    public string ProgrammingLanguage { get; set; } = string.Empty;
}

Step 3: Add the Generic Constraint

public static void DisplayEmployee<T>(T employee)
    where T : Employee
{
    employee.DisplayName();
}

The constraint guarantees that T is Employee or derives from Employee.

Therefore, the DisplayName() method is available inside the generic method.

Step 4: Call the Generic Method

var developer = new Developer
{
    Name = "John",
    ProgrammingLanguage = "C#"
};

DisplayEmployee(developer);

Output

Employee: John

The same method can accept Employee or any class derived from it.

Using an Interface Constraint

An interface constraint restricts the generic type to types that implement a particular interface.

Step 1: Create an Interface

public interface IPrintable
{
    void Print();
}

Step 2: Implement the Interface

public class Employee : IPrintable
{
    public string Name { get; set; } = string.Empty;

    public void Print()
    {
        Console.WriteLine($"Employee: {Name}");
    }
}

Step 3: Add the Interface Constraint

public static void PrintItem<T>(T item)
    where T : IPrintable
{
    item.Print();
}

Because T is constrained to IPrintable, the generic method can safely call Print().

Step 4: Use the Generic Method

var employee = new Employee
{
    Name = "Sarah"
};

PrintItem(employee);

Output

Employee: Sarah

If a class does not implement IPrintable, it cannot be passed to this method.

Combining Multiple Constraints

C# also allows multiple constraints to be applied to a single type parameter.

For example:

public static T CreateEmployee<T>()
    where T : Employee, IPrintable, new()
{
    return new T();
}

This requires T to:

A type argument must satisfy all specified constraints.

Complete Example

The following example brings several concepts together:

using System;

public interface IPrintable
{
    void Print();
}

public class Employee : IPrintable
{
    public string Name { get; set; } = string.Empty;

    public void Print()
    {
        Console.WriteLine($"Employee: {Name}");
    }
}

public class Developer : Employee
{
    public string ProgrammingLanguage { get; set; } = string.Empty;
}

public class GenericHelper
{
    public static T CreateInstance<T>() where T : new()
    {
        return new T();
    }

    public static void DisplayEmployee<T>(T employee)
        where T : Employee
    {
        employee.Print();
    }

    public static void PrintValue<T>(T value)
        where T : struct
    {
        Console.WriteLine($"Value: {value}");
    }
}

class Program
{
    static void Main()
    {
        var developer = GenericHelper.CreateInstance<Developer>();

        developer.Name = "John";
        developer.ProgrammingLanguage = "C#";

        GenericHelper.DisplayEmployee(developer);

        GenericHelper.PrintValue(100);
        GenericHelper.PrintValue(25.5);
    }
}

Output

When the application is executed, the output is:

Employee: John
Value: 100
Value: 25.5

The example demonstrates how constraints allow the generic methods to work only with types that satisfy their requirements.

Generic Constraints and Compile-Time Safety

One of the biggest advantages of generic constraints is that invalid type arguments can be detected during compilation.

For example:

GenericHelper.PrintValue("Hello");

This is invalid because the PrintValue method requires:

where T : struct

Similarly, a method constrained to Employee cannot accept an unrelated class.

This means developers can discover many type-related errors before the application runs.

Generic Constraints vs Runtime Type Checking

Without generic constraints, developers may be tempted to use runtime checks:

if (value is Employee employee)
{
    employee.Print();
}

A generic constraint provides a stronger contract:

public static void DisplayEmployee<T>(T employee)
    where T : Employee
{
    employee.Print();
}

The second approach communicates the requirement directly through the method signature and allows the compiler to enforce it.

Practical Guidelines

When working with generic constraints, consider the following practices:

What About Constraint-Based Prompting in AI?

The term "constraint-based prompting" is also used in AI prompt engineering. In that context, it refers to specifying boundaries for an AI system's response, such as its format, length, audience, scope, or available resources.

For example:

Explain this C# method in 100 words or fewer.
Include the method's purpose, parameters, return value,
and two possible edge cases.

These constraints control the expected format and scope of an AI-generated response.

This concept is different from generic constraints in C#. Generic constraints are compiler-enforced rules for type parameters, while prompt constraints are instructions provided to an AI system.

Conclusion

Generic constraints are an important feature of C# generics. They allow developers to restrict the types that can be used with generic classes and methods while improving compile-time type safety.

In this article, we explored the commonly used constraints:

We also saw how multiple constraints can be combined and how they allow generic code to safely access required members.

By choosing appropriate generic constraints, developers can create reusable APIs that remain flexible while clearly defining the capabilities required from their type parameters.