How to Use Generics with Type Constraints to Handle Collections in C#

Recently, I wanted to use a method for two different concrete classes that shared an interface. On the surface, the solution was simple: have the method parameter typed as the interface. If I was only dealing with a single instance of one of these concrete classes, that solution would have been sufficient. I was dealing with a list of these objects, though, and specifying the parameter of my method as a list of the interface type would not work. That’s because the parameter passed was typed as the concrete class and not the interface. Here’s how I used a generic method with type constraints to solve this predicament.

Context

We will start by defining our problem space. First, we have a simple interface:

public interface IExampleInterface
{
    string Property1 { get; set; }
    string Property2 { get; set; }
}

We’ll also define a pair of concrete classes that will extend our interface:

public class ConcreateA : IExampleInterface { ... }
public class ConcreateB : IExampleInterface { ... }

Now, let’s say we have a list of these classes. Additionally, for reasons outside our control, those lists need to be typed each as their respective concrete class rather than their common interface:

public void Run()
{
    //...
    List <ConcreateA> listA = GetListA();
    List <ConcreateB> listB = GetListB();
    //...
}

Next, say we were now to create a method that only took a single instance of either of these concrete classes but only interacted with the common interface they share. In that case, we could do something like this:

public void ExampleMethod(IExampleInterface item)
{
    //... do something
}

Suppose we were to iterate over listA or listB and pass each item into our method. In that case, the compiler will be perfectly happy despite the objects being typed as their concrete implementation.

However, if we needed to do the iteration inside our method, simply changing the parameter type might seem like the most straightforward solution:

public void ExampleMethod(List<IExampleInterface> list)
{
    //... do something
}

Had our listA and listB been typed as the interface, this solution would work just fine. But the constraint in this situation is that we, for whatever reason, need our lists typed as concrete versions of themselves. So now we have something like this:

public void Run()
{
    //...
    List <ConcreateA> listA = GetListA();
    List <ConcreateB> listB = GetListB();

    ExampleMethod(listA);
    ExampleMethod(listB);
    //...
}

Our compiler is very unhappy and gives us an error that roughly translates to: “We can’t find a method by that name that takes a parameter of that type.” The compiler is not accepting us passing a list of a concrete type to a method that expects a list of the interface, even though our concrete extends said interface.

The Solution: Generic Methods with Type Constraints

In .NET, generics allow the user to create methods (and classes) that have the type specified when the method is used. For example:

public void GenericMethod<T>(T iWillBeOfTypeT)
{
    //...   
}

public void Run()
{
    //...
    int iAmAnInt = 7;
    GenericMethod<int>(iAmAnInt);
    
    string iAmAString = "Atomic Object";
    GenericMethod<string>(iAmAString);
    //...
}

We can use `GenericMethod` with any type, and that’s very powerful! Now, let’s pare back which types `T` can be specified as. We can do that with type constraints. We can start by updating our example method to be generic:

public void ExampleMethodWithGenerics<T>(List<T> list) 
{
 //...   
}

Our list parameter is now of type T to match the type of our generic method. Given the example ExampleMethodWithGenerics<string>(someList), someList must be a list of strings.

Let’s add our type constraint so that ExampleMethodWithGenerics can only be typed as a class that implements the interface IExampleInterface:

private void ExampleMethodWithGenerics<T>(List<T> list) where T: IExampleInterface
{
 //...   
}

Lastly, we can go back and update our Run method:

public void Run()
{
    //...
    List <ConcreateA> listA = GetListA();
    List <ConcreateB> listB = GetListB();

    ExampleMethodWithGenerics(listA);
    ExampleMethodWithGenerics(listB);
    //...
}

With these changes, our compiler is now much happier with us!