Classic Example for C# Generics: implementing a method differently to return different types

Till date, C# Generics is one of my favourite concepts. I still feel Generics is one timeless concept that is handy to show your technical prowess while solving complicated scenarios at the same time.  Like any technical concept, the more we implement a concept, we become better at it. So, here is one such practical situation I encountered and I felt it deserves a blog post as an eulogy for C# generics implementation. Since I'm also a long-time fan of GoF design patterns, Generics is used here as part of Factory design pattern to return two different objects from one single method implemented differently as part of the same interface when instantiated differently through Dependency Injection(DI).

For some background, with my earlier two blog posts as the basis, for creating sitemap xml in two different ways, there are two different object models required to access the Sitemap GraphQL resultset in the asp.net core rendering host side.

1. Generate/Store Sitemap xml in CMS and retrieve on rendering host request

2. Generate and display Sitemap.xml on rendering host request

Complex return type for first approach:

############

namespace SitemapXmlModel
{
public class SitemapXmlData
{
public SearchXmlData Search { get; set; }
}
public class PageInfo
{
public string endCursor { get; set; }
public bool hasNext { get; set; }
}
public class XmlResult
{
public SitemapXml SitemapXml { get; set; }
}
public class SitemapXml
{
public string Value { get; set; }
}
public class Root
{
public SitemapXmlData Data { get; set; }
}
public class SearchXmlData
{
public int total { get; set; }
public PageInfo pageInfo { get; set; }
public List<XmlResult> Results { get; set; }
}
public class Updateddatetime
{
public string Value { get; set; }
}
public class Url
{
public string Path { get; set; }
}
}

############

Complex return type for second approach:

###########

namespace SitemapDataModel
{
public class ChangeFrequency
{
public TargetItem TargetItem { get; set; }
}
public class SitemapData
{
public SearchData Search { get; set; }
}
public class PageInfo
{
public string endCursor { get; set; }
public bool hasNext { get; set; }
}
public class Priority
{
public TargetItem TargetItem { get; set; }
}
public class Result
{
public Updateddatetime UpdatedDatetime { get; set; }
public Url Url { get; set; }
public string Name { get; set; }
public Priority Priority { get; set; }
public ChangeFrequency ChangeFrequency { get; set; }
}
public class SitemapXml
{
public string Value { get; set; }
}
public class Root
{
public SitemapData Data { get; set; }
}
public class SearchData
{
public int total { get; set; }
public PageInfo pageInfo { get; set; }
public List<Result> Results { get; set; }
}
public class TargetItem
{
public string DisplayName { get; set; }
}
public class Updateddatetime
{
public string Value { get; set; }
}
public class Url
{
public string Path { get; set; }
}
}

###########

One step backward, without generics in place,  you will need two different methods with same structure but different return types:

Diagrammatic representation of the scenario:

Github code for reference

The above code clearly violates the SOLID architecture' Open-Closed(OC) principle of open to extension but closed to changes. In summary, the CustomGraphQlServiceHandler class is a bottle neck. So, this is where Generics steps in to the rescue. With some minor refactoring, the GetSitemap becomes a base method that can be overridden as desired to produce the respective object for the GraphQL resultset. Retaining the return types as-is (for the first approach is named Result while the return type for the second approach is named XmlResult), this is the technical design with Generics in place:


So, here is the new code:

Base Class:

############
public abstract class CustomGraphQlLayoutBaseServiceHandler<T>
{
public abstract Task<List<T>> GetSitemap();
}
############

Child Class-1:

############
public class CustomGraphQlSitemapUrlServiceHandler : CustomGraphQlLayoutBaseServiceHandler<Result>
{
private readonly IGraphQLRequestBuilder _graphQLRequestBuilder;
private readonly IGraphQLClientFactory _graphQLClientFactory;
private readonly MvpSiteSettings _configuration;
private readonly Task<List<Result>> result;
public CustomGraphQlSitemapUrlServiceHandler(IConfiguration configuration, IGraphQLRequestBuilder graphQLRequestBuilder, IGraphQLClientFactory graphQLClientFactory)
{
_graphQLRequestBuilder = graphQLRequestBuilder;
_graphQLClientFactory = graphQLClientFactory;
_configuration = configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>();
}
public async override Task<List<Result>> GetSitemap()
{
var client = _graphQLClientFactory.CreateGraphQlClient();
var query = Constants.GraphQlQueries.GetSitemapQuery;
var variables = (object)new
{
rootItemId = _configuration.RootItemId,
language = _configuration.DefaultLanguage
};
var request = _graphQLRequestBuilder.BuildRequest(query, variables);
var graphQlResponse = await client.SendQueryAsync<SitemapData>(request);
return graphQlResponse.Data.Search.Results;
}
}
############

Child Class-2:

###########

public class CustomGraphQlSitemapXmlServiceHandler : CustomGraphQlLayoutBaseServiceHandler<XmlResult>
{
private readonly IGraphQLRequestBuilder _graphQLRequestBuilder;
private readonly IGraphQLClientFactory _graphQLClientFactory;
private readonly MvpSiteSettings _configuration;
private Task<List<XmlResult>> result;
public CustomGraphQlSitemapXmlServiceHandler(IConfiguration configuration, IGraphQLRequestBuilder graphQLRequestBuilder, IGraphQLClientFactory graphQLClientFactory)
{
_graphQLRequestBuilder = graphQLRequestBuilder;
_graphQLClientFactory = graphQLClientFactory;
_configuration = configuration.GetSection(MvpSiteSettings.Key).Get<MvpSiteSettings>();
}
public async override Task<List<XmlResult>> GetSitemap()
{
var client = _graphQLClientFactory.CreateGraphQlClient();
var query = Constants.GraphQlQueries.GetSitemapSearchQuery;
var variables = (object)new
{
itemId = _configuration.SitemapXmlItemId,
language = _configuration.DefaultLanguage
};
var request = _graphQLRequestBuilder.BuildRequest(query, variables);
var graphQlResponse = await client.SendQueryAsync<SitemapXmlData>(request);
return graphQlResponse.Data.Search.Results;
}
}

###########

With such a structure in place and Dependency Injection in action, instantiating separate class for each approach, will automatically invoke the corresponding method and create the correct object model.

MvpSitemapXmlDataProvider Class method:

MvpSitemapUrlProvider Class method:


Diagrammatic representation as to how this solution confirms to SOLID architecture' Open-Closed(OC) principle:

Github Commit

Reference:

https://stackoverflow.com/questions/25211123/generic-method-that-can-return-different-types

Comments