4
\$\begingroup\$

I have been working on creating maintainable networking layer for our Asp.Net Core Web Api project. Right now I have created Generic methods for Get, Post, Put like this.

 public class BaseClient
{
    private HttpClient _client;
    private ILogger<BaseClient> _logger;
    private string AuthToken;
    public BaseClient(HttpClient client, ILogger<BaseClient> logger, AuthenticationHeader authHeader)
    {
        _client = client;
        client.BaseAddress = new Uri("url/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
        _logger = logger;
        AuthToken = authHeader.AuthHeader;
    }
    public async Task<FailureResponseModel> GetFailureResponseModel(HttpResponseMessage response)
    {
        FailureResponseModel failureModel = await response.Content.ReadAsAsync<FailureResponseModel>();
        failureModel.ResponseStatusCode = Convert.ToInt32(response.StatusCode);
        _logger.LogError("Request Failed: {Error}", failureModel.ResultDetails);
        return failureModel;
    }

    public async Task<object> ProcessAsync<T>(HttpRequestMessage request, NamingStrategy namingStrategy)
    {
        if (!string.IsNullOrEmpty(AuthToken))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthToken);
        }
        HttpResponseMessage response = await _client.SendAsync(request);
        if (response.IsSuccessStatusCode)
        {
            _logger.LogInformation("Request Succeeded");
            var dezerializerSettings = new JsonSerializerSettings
            {
                ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = namingStrategy
                }
            };
            T responseModel = JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync(), dezerializerSettings);
            return responseModel;
        }
        else
        {
            return await GetFailureResponseModel(response);

        }
    }
    public async Task<object> GetAsync<T>(string uri)
    {
        return await GetAsync<T>(uri, new DefaultNamingStrategy());
    }
    public async Task<object> GetAsync<T>(string uri, NamingStrategy namingStrategy)
    {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri))
        {
            return await ProcessAsync<T>(requestMessage, namingStrategy);
        }
    }
    public async Task<object> PostAsync<T1, T2>(string uri, T2 content)
    {
        return await PostAsync<T1, T2>(uri, content, new DefaultNamingStrategy());
    }
    public async Task<object> PostAsync<T1, T2>(string uri, T2 content, NamingStrategy namingStrategy)
    {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
        {
            var json = JsonConvert.SerializeObject(content);
            using (var stringContent = new StringContent(json, Encoding.UTF8, "application/json"))
            {
                requestMessage.Content = stringContent;
                return await ProcessAsync<T1>(requestMessage, namingStrategy);
            }
        }
    }
    public async Task<object> PutAsyc<T1, T2>(string uri, T2 content)
    {
        return await PutAsyc<T1, T2>(uri, content, new DefaultNamingStrategy());
    }
    public async Task<object> PutAsyc<T1, T2>(string uri, T2 content, NamingStrategy namingStrategy)
    {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, uri))
        {
            var json = JsonConvert.SerializeObject(content,
                    Formatting.None,
                    new JsonSerializerSettings
                    {
                        NullValueHandling = NullValueHandling.Ignore
                    });
            using (var stringContent = new StringContent(json, Encoding.UTF8, "application/json"))
            {
                requestMessage.Content = stringContent;
                return await ProcessAsync<T1>(requestMessage, namingStrategy);
            }
        }
    }

As it can be seen I have to write 2 Methods for Get, Post and Put due to SnakeCase and CamelCase Naming Strategies as some response consist of SnakeCase and some responses are in CamelCase

I have created ApiManager kinda class which calls above Class methods like this.

 public class ClubMatasClient
    {
        private BaseClient _client;
        private ILogger<ClubMatasClient> _logger;
        private string AuthToken;
        public ClubMatasClient(BaseClient client, ILogger<ClubMatasClient> logger, AuthenticationHeader authHeader)
        {
            _client = client;
            _logger = logger;
            AuthToken = authHeader.AuthHeader;
        }

        public async Task<object> GetShops(string category)
        {
            _logger.LogInformation("ClubMatas outgoing request: {RequestName}", nameof(GetShops));
            return await _client.GetAsync<ShopsResponseModel>($"v2/shops?category={WebUtility.UrlEncode(category)}");
        }
        public async Task<object> PostLogin(LoginRequestModel form)
        {
            _logger.LogInformation("ClubMatas outgoing request: {RequestName}", nameof(PostLogin));
            return await _client.PostAsync<LoginResponseModel, LoginRequestModel>("v2/login", form, new SnakeCaseNamingStrategy());
        }
}

And this class in being injected in my Controllers and I am using this ApiManager like this to call api and return response.

 public async Task<ActionResult<object>> GetShops([FromQuery(Name = "category")]string category)
    {
        var response = await _httpClient.GetShops(category);
        return ParseResponse<ShopsResponseModel>(response);
    }

ParseResponse is helper Generic method which either return SuccessModel (passed as T) or failure response Model.

 protected ActionResult<object> ParseResponse<T>(object response)
    {
        if (response.GetType() == typeof(T))
        {
            return Ok(response);
        }
        else
        {
            return Error(response);
        }
    }

Now my question is as I am fairly new to C#/ASP.net Core , is my current flow/architecture is fine, and how can I make it more elegant and more flexible.

\$\endgroup\$
  • \$\begingroup\$ I suggest that you look into the IHttpClientFactory interface; it was designed to accomplish exactly what you're doing here but has more features and is maintained by Microsoft =D. \$\endgroup\$ – Kittoes0124 Apr 22 at 17:17
  • \$\begingroup\$ Well I am using the same the only difference is i am using TypedClient \$\endgroup\$ – Shabir jan Apr 22 at 18:48
  • \$\begingroup\$ Ah sorry, the BaseClient class threw me off; give me some time and I'll supply a proper refactor as an answer. \$\endgroup\$ – Kittoes0124 Apr 22 at 18:51
2
\$\begingroup\$

I thought that what you have is actually quite solid; my only suggestion is that you should take advantage of C#'s features in order to slightly clean things up. First, let's create an options object to hold the various items that you're injecting into the class:

public class HttpServiceOptions<TLogger>
{
    public string AuthenticationToken { get; set; }
    public HttpClient Client { get; set; }
    public ILogger<TLogger> Logger { get; set; }
}

This should make it much simpler to add/remove dependencies to the "system" since everything is injected via a single "container". Now, let's make your base class abstract and refactor it to accept our options object:

public abstract class AbstractHttpService<TLogger>
{
    private readonly string _authToken;
    private readonly HttpClient _client;

    public ILogger<TLogger> Logger { get; }

    public AbstractHttpService(HttpServiceOptions<TLogger> options) {
        var client = options.Client;

        client.BaseAddress = new Uri("url/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");

        _authToken = options.AuthenticationToken;
        _client = client;

        Logger = options.Logger;
    }

    public async Task<FailureResponseModel> GetFailureResponseModel(HttpResponseMessage response) {
        var failureModel = await response.Content.ReadAsAsync<FailureResponseModel>();

        failureModel.ResponseStatusCode = Convert.ToInt32(response.StatusCode);

        Logger.LogError("Request Failed: {Error}", failureModel.ResultDetails);

        return failureModel;
    }
    public async Task<object> ProcessAsync<T>(HttpRequestMessage request, NamingStrategy namingStrategy) {
        var authToken = _authToken;

        if (!string.IsNullOrEmpty(authToken)) {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
        }

        var response = await _client.SendAsync(request);

        if (response.IsSuccessStatusCode) {
            Logger.LogInformation("Request Succeeded");

            var dezerializerSettings = new JsonSerializerSettings {
                ContractResolver = new DefaultContractResolver {
                    NamingStrategy = namingStrategy
                }
            };
            var responseModel = JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync(), dezerializerSettings);

            return responseModel;
        }
        else {
            return await GetFailureResponseModel(response);
        }
    }
    public async Task<object> GetAsync<T>(string uri) {
        return await GetAsync<T>(uri, new DefaultNamingStrategy());
    }
    public async Task<object> GetAsync<T>(string uri, NamingStrategy namingStrategy) {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri)) {
            return await ProcessAsync<T>(requestMessage, namingStrategy);
        }
    }
    public async Task<object> PostAsync<T1, T2>(string uri, T2 content) {
        return await PostAsync<T1, T2>(uri, content, new DefaultNamingStrategy());
    }
    public async Task<object> PostAsync<T1, T2>(string uri, T2 content, NamingStrategy namingStrategy) {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)) {
            var json = JsonConvert.SerializeObject(content);

            using (var stringContent = new StringContent(json, Encoding.UTF8, "application/json")) {
                requestMessage.Content = stringContent;
                return await ProcessAsync<T1>(requestMessage, namingStrategy);
            }
        }
    }
    public async Task<object> PutAsyc<T1, T2>(string uri, T2 content) {
        return await PutAsyc<T1, T2>(uri, content, new DefaultNamingStrategy());
    }
    public async Task<object> PutAsyc<T1, T2>(string uri, T2 content, NamingStrategy namingStrategy) {
        using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, uri)) {
            var json = JsonConvert.SerializeObject(
                value: content,
                formatting: Formatting.None,
                settings: new JsonSerializerSettings {
                    NullValueHandling = NullValueHandling.Ignore
                }
            );

            using (var stringContent = new StringContent(json, Encoding.UTF8, "application/json")) {
                requestMessage.Content = stringContent;

                return await ProcessAsync<T1>(requestMessage, namingStrategy);
            }
        }
    }
}


Finally, we implement the ClubMatasHttpService by deriving from the abstract class:

public sealed class ClubMatasHttpService : AbstractHttpService<ClubMatasHttpService>
{
    public ClubMatasHttpService(HttpServiceOptions<ClubMatasHttpService> options) : base(options) { }

    public async Task<object> GetShops(string category) {
        Logger.LogInformation("ClubMatas outgoing request: {RequestName}", nameof(GetShops));

        return await GetAsync<ShopsResponseModel>($"v2/shops?category={WebUtility.UrlEncode(category)}");
    }
    public async Task<object> PostLogin(LoginRequestModel form) {
        Logger.LogInformation("ClubMatas outgoing request: {RequestName}", nameof(PostLogin));

        return await PostAsync<LoginResponseModel, LoginRequestModel>("v2/login", form, new SnakeCaseNamingStrategy());
    }
}
\$\endgroup\$
  • \$\begingroup\$ Thank you so much, it really cleanup stuff :) Just one question which is really bugging me, you can as i am expecting 2 different models in case of success and failure, i am setting return type as object. Is there any other elegant way to fix this? \$\endgroup\$ – Shabir jan Apr 22 at 19:30
  • \$\begingroup\$ @Shabirjan I think we can, once I finish up some actual work here I'll play around with the code a bit more and see what happens. \$\endgroup\$ – Kittoes0124 Apr 22 at 19:34
  • \$\begingroup\$ Sure and thanks again :) waiting to hear from you soon \$\endgroup\$ – Shabir jan Apr 22 at 19:35
  • \$\begingroup\$ A question how would I register and inject dependencies by using that options class? \$\endgroup\$ – Shabir jan Apr 23 at 5:27
  • \$\begingroup\$ @Shabirjan Sorry for the delay, busy with other things but I promise I will get back to ya on that; in addition to the generic stuffs. \$\endgroup\$ – Kittoes0124 Apr 23 at 5:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.