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
, orCompanyConfigurationResponse
(there were moreResponse
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!