Be careful with generic overloads in C#

Be careful with generic overloads in C#

2022, Jan 28    

Generics help to generalize the logic and avoid creating multiple overloads for the same logic if the types can be combined by common conditions. This works great as long as it doesn’t involve other generic overloads nearby.

Let’s consider this case.

Consider having a class used as a standardized response from the server:

public class SuccessResponse<TItem>
{
  public IEnumerable<TItem>? Items { get; init; }
}

And a class that is a factory for IActionResult from the response above:

public class ResponseResultFactory
{
  public IActionResult Create<TItem>(IEnumerable<TItem> items)
  {
    var response = new SuccessResponse<TItem>
    {
      Items = items
    };
    
    return OkObjectResult(response);
  }

  public IActionResult Create<TItem>(TItem item)
  {
    return Create(new[] { item });
  }
}

We have added 2 methods, the first for the collection of returned objects, and the second for 1 single element, so as not to wrap it in a collection each time.

At first glance, the code looks simple and predictable. Calling the second method should, as expected, call the first method, because TItem[] can be easily cast to IEnumerable<TItem> implicitly.

However, there is an insidious feature of overloads here, namely, the search order for methods for overloads in the case of generic types. Try running the second method from the code above - you’ll end up with infinite recursion. The second method will not call the first method, but itself, treating the array as an object of type TItem.

This can be visualized as follows. Using TItem as string as an example:

  Create<string>(string item) ->
  Create<string[]>(string[] item) ->
  Create<string[][]>(string[][] item) ->
  ...

Each iteration of the recursion wraps the previous result of the method in an array. But why is this happening?

It’s a matter of type inference for such overloads. If we remove the sugar for generic types inference from the code above, we get the following code for the second method:

return Create<TItem[]>(new[] { item });

Instead of expected

return Create<TItem>(new[] { item });

Therefore, in order for the code above to work correctly, you need to specify the generic type explicitly, prompting the compiler for the correct overload.

Full working factory code:

public class ResponseResultFactory
{
  public IActionResult Create<TItem>(IEnumerable<TItem> items)
  {
    var response = new SuccessResponse<TItem>
    {
      Items = items
    };
    
    return OkObjectResult(response);
  }

  public IActionResult Create<TItem>(TItem item)
  {
    return Create<TItem>(new[] { item });
  }
}

Conclusion

Try to avoid type inference in generic methods and overloads as much as possible. Of course, if your project has good enough test coverage - there is nothing to worry about, you will find the error much earlier, even if it is not you who make it.

However, in the absence of tests (always write tests!) such a feature is almost impossible to notice during code review.

If you like my work or want a special kind of technical support for one of the projects I made, you could support me on GitHub! :heart:

Sponsor seclerp