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.
1 Answer 1
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);
}
}
-
\$\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– Shabir jan2019年04月22日 19:30:42 +00:00Commented 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\$Kittoes0124– Kittoes01242019年04月22日 19:34:22 +00:00Commented Apr 22, 2019 at 19:34
-
\$\begingroup\$ Sure and thanks again :) waiting to hear from you soon \$\endgroup\$Shabir jan– Shabir jan2019年04月22日 19:35:12 +00:00Commented Apr 22, 2019 at 19:35
-
\$\begingroup\$ A question how would I register and inject dependencies by using that options class? \$\endgroup\$Shabir jan– Shabir jan2019年04月23日 05:27:40 +00:00Commented 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\$Kittoes0124– Kittoes01242019年07月12日 16:12:46 +00:00Commented Jul 12, 2019 at 16:12
BaseClient
class threw me off; give me some time and I'll supply a proper refactor as an answer. \$\endgroup\$