Using Generics and Interfaces to Simplify C# Code

Overview

Background

If you're working in .NET or other languages that support interfaces and generics, you've probably used them. Finding reasons to create them can be more challenging, but I recently came across two completely different instances that clearly benefited from the interface / generic combo and thought they would be worth sharing. Enjoy! :)

Example 1: Simplify Common Code Assignments

I recently came across an API that was using a very consistent pattern of including a List<ErrorInfo> in ...Response objects to include validation errors or other errors that might have occurred.

When errors would occur that would set those properties, the code looked something like this (note the many lines of code and semi-blank lines with just curly-braces):

 1try
 2{
 3    ///... ommitted for brevity
 4    return new SecurityQuestionsResponse
 5    {
 6        ErrorInfo = new List<ErrorInfo>()
 7        {
 8            new ErrorInfo()
 9            {
10                Code = "USERAPI_ERROR",
11                Message = Messages.USERAPI_ERROR + " - Null respInfo" 
12            }
13        }
14    };
15}
16catch (Exception)
17{
18    return new SecurityQuestionsResponse
19    {
20        ErrorInfo = new List<ErrorInfo>()
21        {
22            new ErrorInfo()
23            {
24                Code = "USERAPI_ERROR",
25                Message = Messages.USERAPI_ERROR
26            }
27        }
28    };
29}

There were many instances like this throughout the code I was seeing and the observation I had was this:

Too much "noise" is created by this code which makes it harder and clumsier to read the "real" logic of the code.

There were many classes that looked similar that created this same kind of "noise" when reporting errors:

 1public class SecurityQuestionsResponse
 2{
 3    //... ommitted
 4    public List<ErrorInfo>? ErrorInfo { get; set; }
 5}
 6
 7public class ApplicationSettingsResponse
 8{
 9    //... ommitted
10    public List<ErrorInfo>? ErrorInfo { get; set; }
11}
12public class CompanyConfigurationResponse
13{
14    //... ommitted
15    public List<ErrorInfo>? ErrorInfo { get; set; }
16}

The next observation is that the repeated code to set the different error responses basically had three moving parts or variables:

  • the Code string,
  • the Message string
  • type of the Response - SecurityQuestionsResponse, ApplicationSettingsResponse, or CompanyConfigurationResponse (there were more Response examples, but you get the point)

Step 1: Create an Interface that marks common traits

The first step in simplifying this was to define a new interface that identifies the commonality of the Response classes, as follows:

1public interface IHaveErrorInfo
2{
3    List<ErrorInfo>? ErrorInfo { get; set; }
4}

Then just add the interface marker to each of the classes:

1public class SecurityQuestionsResponse : IHaveErrorInfo { ... }
2
3public class ApplicationSettingsResponse : IHaveErrorInfo { ... }
4
5public class CompanyConfigurationResponse : IHaveErrorInfo { ... }

Step 2: Create a Static Generic Method

With the interface in place, a new static generic method can be defined. Note that the T argument is constrained to be types that implement the IHaveErrorInfo interface (and requires a parameterless constructor).

 1public static class QuickError
 2{
 3    public static T Create<T>(string code, string message) where T : IHaveErrorInfo, new()
 4    {
 5        return new T
 6        {
 7            ErrorInfo = new List<ErrorInfo>()
 8            {
 9                new() { Code = code, Message = message }
10            }
11        };
12    }
13}

Step 3: Simplify Original Code

With the interface and the new generic method in place, the original code from the snippet above can be simplified to what follows:

 1try
 2{
 3    ///... ommitted for brevity
 4    return QuickError.Create<SecurityQuestionsResponse>("USERAPI_ERROR",
 5        Messages.USERAPI_ERROR + " - Null respInfo");
 6}
 7catch (Exception)
 8{
 9    return QuickError.Create<SecurityQuestionsResponse>("USERAPI_ERROR",
10        Messages.USERAPI_ERROR);
11}

Example 2: Encapsulate Paginated API Response-Handling logic

I was calling various methods of an API that had hard limits of response content of 50 items per call. I needed the full results of the various calls, and the offset / limit functionality of each endpoint was the same.

I had code in different places that looked like this:

 1var fieldsToReturn = "id,name,description,updated"
 2
 3bool needToGetMoreItems;
 4var offset = 0;
 5const int limit = 50;
 6do
 7{
 8    var queryString = $"fields={fieldsToReturn}&limit={limit}&offset={offset}{filter}";
 9    var response = await client.GetFromJsonAsync<ReleaseApiResponse>($"api/v3/releases?{queryString}");
10    if (response?.Items != null)
11    {
12        releases.AddRange(response.Items);
13    }
14    needToGetMoreItems = (response?.Items.Count ?? 0) == limit;
15    offset += response?.Items.Count ?? 0;
16} while (needToGetMoreItems);

For the true "moving parts" of the above code, the things that were different were as follows:

  • API route (api/v3/releases in the example)
  • fieldsToReturn : this represented a list of the fields I wanted back from the API
  • The response type of the API (ReleaseApiResponse in the example)
  • The type of the Items property of the ...Response

Step 1: Define an Interface for the Shared Traits of the Responses

The different API responses all had an Items collection of a different type, so creating an interface to define that was first.

1public interface IHaveItems<T>
2{
3    List<T> Items { get; set; }
4}

Then I could add the interface to the different API response types:

 1public record ApplicationResponse : IHaveItems<Application>
 2{
 3    public List<Application> Items { get; set; } = new();
 4}
 5public record CompanyResponse : IHaveItems<Company>
 6{
 7    public List<Company> Items { get; set; } = new();
 8}
 9public record RelaseResponse : IHaveItems<Release>
10{
11    public List<Release> Items { get; set; } = new();
12}

Step 2: Define Generic Method that Encapsulates the Pagination Logic

The generic method I defined had two generics, the top response object and the instance object, and the response object was constrained to need an Items property that was a List of the instance object.

 1public static class PaginatedApiHelper
 2{
 3    public static async Task<List<T>> GetAllDataFromPaginatedApi<T, T1>(this HttpClient client, 
 4        string route, string fieldsToReturn, string? filter = null) where T1 : IHaveItems<T>
 5    {
 6        var fullResults = new List<T>();
 7        
 8        bool needToGetMoreItems;
 9        var offset = 0;
10        const int limit = 50;
11        do
12        {
13            var queryString = $"fields={fieldsToReturn}&limit={limit}&offset={offset}{filter}";
14            var response = await client.GetFromJsonAsync<T1>($"{route}?{queryString}");
15            if (response?.Items.Any() ?? false)
16            {
17                fullResults.AddRange(response.Items);
18            }
19            needToGetMoreItems = (response?.Items?.Count ?? 0) == limit;
20            offset += response?.Items?.Count ?? 0;
21        } while (needToGetMoreItems);
22
23        return fullResults;
24    }
25}

Step 3: Simplify Original Code

With the interface and method in place, the original code above is simplified to this:

1var fieldsToReturn = "id,name,description,updated"
2
3var releases = client.GetAllDataFromPaginatedApi<Release, ReleaseApiResponse>("api/v3/releases", fieldsToReturn);

With the many times that my paginated logic was repeated for different calls, this made things a lot simpler to read!

Comments? Leave them below. Thanks for reading!