How to create efficient HttpClient for multiple clients using HttpClientFactory?

152 views Asked by At

This is a best practice question.

I am using HttpClientFactory to create HttpClient. There is a scenario where I might end up doing similar for other client integration. So rather than duplicate logic in two places, I want to keep it centralized.

But I do not know if having common place for Client would be good practice in case of higher volume of calls or not. This is where I need some help on how to scale and do not keep duplicate code if possible. As you can see in below code, ApiClientOne and ApiClientTwo has same private function for request. What I want is, have the centralized place for that and keep N clients calling that.

  • If I move async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null) to common place and have N clients call it then, would it scale?
  • Is there a better way to achieve the desired result?
  • Or should I keep these separate? (the way it is now)

Code:

Program.cs

.ConfigureServices((_, services) =>
{
    services.AddApiClientOne(config);
    services.AddApiClientTwo(config);
})

ServiceContainer.cs

namespace Configuration
{
    [ExcludeFromCodeCoverage]
    public static class ServiceContainer
    {
        public static void AddApiClientTwo(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddHttpClient<IApiClientTwo, ApiClientTwo>((serviceProvider, httpClient) =>
            {
                httpClient.BaseAddress = new Uri(configuration.GetSettings("Services:ApiClientTwo:BaseUrl", ""));
                httpClient.Timeout = TimeSpan.FromSeconds(30);
                httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
                    serviceProvider.GetRequiredService<IInternalAuthClient>().GetTokenAsync()
                        .GetAwaiter().GetResult());
            });
        }

        public static void AddApiClientOne(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddHttpClient<IApiClientOne, ApiClientOne>("ApiClientOne", options =>
            {
                options.BaseAddress = new Uri(configuration.GetSettings("Services:ClientOne:BaseUrl", string.Empty, false));
                options.DefaultRequestHeaders.Add("clientId", configuration[Configurations.ClientOneClientId]);
                options.SetBasicAuthentication(configuration[Configurations.ClientOneUsername],
                    configuration[Configurations.ClientOnePassword]);
            });
            
        }

     }
}

ApiCleintOne.cs

namespace Clients.ApiClientOne;

public class ApiClientOne : IApiClientOne
{
    private readonly ILogger<ApiClientOne> _logger;
    private readonly HttpClient _httpClient;
    private Lazy<JsonSerializerOptions> _settings;
    
    public ApiClientOne(HttpClient httpClient, ILogger<ApiClientOne> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings);
    }
    
    public async Task<GetSomethingResponse> GetSomething(string value)
    {
        var response = await Request<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}");
        return response;
    }
    
    private JsonSerializerOptions CreateSerializerSettings()
    {
        var settings = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        return settings;
    }

    private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)
    {
        var client = _httpClient;
        var disposeClient = false;

        using var request = new HttpRequestMessage();
        var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value));
        content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        request.Content = content_;
        request.Method = method;
        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
        request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute);

        var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var disposeResponse_ = true;
        
        try
        {
            var content = await response.Content.ReadAsStringAsync();
            var headers =
                Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value);
            if (response.Content != null && response.Content.Headers != null)
            {
                foreach (var item in response.Content.Headers)
                    headers[item.Key] = item.Value;
            }

            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"some error.";
                _logger.Error($"{errorMessage} {{ @content }}", args: new object[] {content});
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<T>(content);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int) ex.StatusCode &&
                                              (int) ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
        finally
        {
            if (disposeResponse_)
            {
                response.Dispose();
                client.Dispose();
            }
        }
    }
}

ApiClientTwo.cs

namespace Clients.ApiClientTwo;

public class ApiClientTwo : IApiClientTwo
{
    private readonly ILogger<ApiClientTwo> _logger;
    private readonly HttpClient _httpClient;
    private Lazy<JsonSerializerOptions> _settings;
    
    public ApiClientTwo(HttpClient httpClient, ILogger<ApiClientTwo> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings);
    }
    
    public async Task<PostSomethingResponse> PostSomething(MyModel value)
    {
        var response = await Request<PostSomethingResponse>(HttpMethod.Post, $"/postSomething", value);
        return response;
    }
    
    private JsonSerializerOptions CreateSerializerSettings()
    {
        var settings = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        return settings;
    }

    private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)
    {
        var client = _httpClient;
        var disposeClient = false;

        using var request = new HttpRequestMessage();
        var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value));
        content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        request.Content = content_;
        request.Method = method;
        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
        request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute);

        var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var disposeResponse_ = true;

        try
        {
            var content = await response.Content.ReadAsStringAsync();
            var headers =
                Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value);
            if (response.Content != null && response.Content.Headers != null)
            {
                foreach (var item in response.Content.Headers)
                    headers[item.Key] = item.Value;
            }

            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"some error.";
                _logger.Error($"{errorMessage} {{ @content }}", args: new object[] { content });
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<T>(content);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int)ex.StatusCode &&
                                              (int)ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
        finally
        {
            if (disposeResponse_)
            {
                response.Dispose();
                client.Dispose();
            }
        }
    }
}
1

There are 1 answers

2
Stephen Cleary On BEST ANSWER

Yes, you can factor out this common method into a static helper type, possibly as an extension method on HttpClient. This will not affect scalability at all.

Example extension method:

static class MyHttpClientExtensions
{
    public static async Task<T> MyRequestAsync<T>(this HttpClient client, HttpMethod method, string endpoint, object payload = null)
    {
        var disposeClient = false;

        using var request = new HttpRequestMessage();
        var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value));
        content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        request.Content = content_;
        request.Method = method;
        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
        request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute);

        var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var disposeResponse_ = true;

        try
        {
            var content = await response.Content.ReadAsStringAsync();
            var headers =
                Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value);
            if (response.Content != null && response.Content.Headers != null)
            {
                foreach (var item in response.Content.Headers)
                    headers[item.Key] = item.Value;
            }

            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"some error.";
                _logger.Error($"{errorMessage} {{ @content }}", args: new object[] { content });
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<T>(content);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int)ex.StatusCode &&
                                              (int)ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
        finally
        {
            if (disposeResponse_)
            {
                response.Dispose();
                client.Dispose();
            }
        }
    }
}

With the extension method above, both your consumers can look like this:

public async Task<GetSomethingResponse> GetSomething(string value)
{
    var response = await _httpClient.MyRequestAsync<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}");
    return response;
}