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.

asked Apr 22, 2019 at 11:33
\$\endgroup\$
4
  • \$\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\$ Commented Apr 22, 2019 at 17:17
  • \$\begingroup\$ Well I am using the same the only difference is i am using TypedClient \$\endgroup\$ Commented Apr 22, 2019 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\$ Commented Apr 22, 2019 at 18:51
  • \$\begingroup\$ You have a typo PutAsyc (I think) \$\endgroup\$ Commented Jul 12, 2019 at 16:32

1 Answer 1

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 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(HttpClient httpClient, IOptions<HttpServiceOptions<TLogger>> options) {
 var optionsValue = options.Value;
 var client = httpClient;
 client.BaseAddress = new Uri("url/");
 client.DefaultRequestHeaders.Add("Accept", "application/json");
 _authToken = optionsValue.AuthenticationToken;
 _client = client;
 Logger = optionsValue.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);
 }
 }
 }
}


We also need to implement the ClubMatasHttpService by deriving from the abstract class:

public sealed class ClubMatasHttpService : AbstractHttpService<ClubMatasHttpService>
{
 public ClubMatasHttpService(HttpClient httpClient, IOptions<HttpServiceOptions<ClubMatasHttpService>> options) : base(httpClient, 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());
 }
}

Finally, we write a couple of extension methods to help configure everything:

public static class IServiceCollectionExtensions
{
 public static IHttpClientBuilder AddClubMatasHttpService(this IServiceCollection services, Action<HttpServiceOptions<ClubMatasHttpService>> configureOptions) {
 if (null == services) { throw new ArgumentNullException(nameof(services)); }
 if (null == configureOptions) { throw new ArgumentNullException(nameof(configureOptions)); }
 services.Configure(configureOptions);
 return services.AddHttpClient<ClubMatasHttpService>();
 }
 public static IHttpClientBuilder AddClubMatasHttpService(this IServiceCollection services, IConfiguration configuration) {
 return services.AddClubMatasHttpService(configuration.Bind);
 }
}
answered Apr 22, 2019 at 19:24
\$\endgroup\$
6
  • \$\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\$ Commented Apr 22, 2019 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\$ Commented Apr 22, 2019 at 19:34
  • \$\begingroup\$ Sure and thanks again :) waiting to hear from you soon \$\endgroup\$ Commented Apr 22, 2019 at 19:35
  • \$\begingroup\$ A question how would I register and inject dependencies by using that options class? \$\endgroup\$ Commented Apr 23, 2019 at 5:27
  • 1
    \$\begingroup\$ @Shabirjan I have updated my answer so that configuration is handled through ASP.NET's DI system and added some extensions that demo possible usage. \$\endgroup\$ Commented Jul 12, 2019 at 16:12

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.