Home > front end >  Can the httpclient factory wire up be re-used
Can the httpclient factory wire up be re-used

Time:10-11

I have a third party api I am using for a number of my Services in .NET Core Web API project.
So, I have something like below:

services.AddHttpClient<IUserService, UserService>(client =>
{
    client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                                 throw new ArgumentNullException("EXTERNAL_SERVICE_BASE_URLcannot be null"));

    client.DefaultRequestHeaders.Add("removed", "removed");

}).ConfigurePrimaryHttpMessageHandler(() =>
{
    HttpClientHandler clientHandler = new HttpClientHandler
    {
        ClientCertificateOptions = ClientCertificateOption.Manual,
        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
    };
    clientHandler.ClientCertificates.Add(new X509Certificate2(securitySession.CertificateData.Data, securitySession.CertificateData.Password));
    return new HttpClientXRayTracingHandler(clientHandler);
})
.AddPolicyHandler(HttpPolicies.GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromSeconds(1));

This is fine and I can access the HttpClient in UserService and call my External API fine. However what I am going to need now is more Services in my code - but they use the same EXTERNAL_API - so lets say and AccountService and a CustomerService - and i'll end up with this:

services.AddHttpClient<IAccountService, AccountService>(client =>
{
    client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                                 throw new ArgumentNullException("EXTERNAL_SERVICE_BASE_URLcannot be null"));
    client.DefaultRequestHeaders.Add("removed", "removed");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    HttpClientHandler clientHandler = new HttpClientHandler
    {
        ClientCertificateOptions = ClientCertificateOption.Manual,
        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
    };
    clientHandler.ClientCertificates.Add(new X509Certificate2(securitySession.CertificateData.Data, securitySession.CertificateData.Password));
    return new HttpClientXRayTracingHandler(clientHandler);
})
.AddPolicyHandler(HttpPolicies.GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromSeconds(1));
services.AddHttpClient<ICustomerService, CustomerService>(client =>
{
    client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                                 throw new ArgumentNullException("EXTERNAL_SERVICE_BASE_URLcannot be null"));
    client.DefaultRequestHeaders.Add("removed", "removed");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    HttpClientHandler clientHandler = new HttpClientHandler
    {
        ClientCertificateOptions = ClientCertificateOption.Manual,
        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
    };
    clientHandler.ClientCertificates.Add(new X509Certificate2(securitySession.CertificateData.Data, securitySession.CertificateData.Password));
    return new HttpClientXRayTracingHandler(clientHandler);
})
.AddPolicyHandler(HttpPolicies.GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromSeconds(1));

The only change being the Service in my App that is using the HttpClient. Is there a way I can move all the common plumbing of the wire up to a private method that can be called when I am adding the http client to each of my services?

CodePudding user response:

I would suggest to have a single named client

services.AddHttpClient("commonClient", client =>
{
    client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                                 throw new ArgumentNullException("EXTERNAL_SERVICE_BASE_URL cannot be null"));

    client.DefaultRequestHeaders.Add("removed", "removed");

}).ConfigurePrimaryHttpMessageHandler(() =>
{
    HttpClientHandler clientHandler = new HttpClientHandler
    {
        ClientCertificateOptions = ClientCertificateOption.Manual,
        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
    };
    clientHandler.ClientCertificates.Add(new X509Certificate2(securitySession.CertificateData.Data, securitySession.CertificateData.Password));
    return new HttpClientXRayTracingHandler(clientHandler);
})
.AddPolicyHandler(HttpPolicies.GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromSeconds(1));

and 3 typed clients

services.AddHttpClient<IUserService, UserService>();
services.AddHttpClient<IAccountService, AccountService>();
services.AddHttpClient<ICustomerService, CustomerService>();

The usage of these components are a bit different than using just a named or just a typed client

readonly IUserService client;
public XYZController(IHttpClientFactory namedClientFactory, ITypedHttpClientFactory<UserService> typedClientFactory)
{
    var namedClient = namedClientFactory.CreateClient("commonClient");
    client = typedClientFactory.CreateClient(namedClient);
}
  • First via the IHttpClientFactory we retrieve the "commonClient"
  • Then we create a new instance of UserService by passing the previously retrieved named client

NOTE: The type parameter of ITypedHttpClientFactory must be the concrete type not the interface otherwise you would receive an InvalidOperationException

CodePudding user response:

You can create an IServiceCollection extension method:

public static class ServiceCollectionExtensions
{
    // Change securitySession parameter to the appropriate type
    public static void AddCustomHttpClient<TClient, TImplementation>(this IServiceCollection services, SecuritySession securitySession)
        where TClient : class
        where TImplementation : class, TClient
    {
        services.AddHttpClient<TClient, TImplementation>(client =>
        {
            client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                                         throw new ArgumentNullException("EXTERNAL_SERVICE_BASE_URLcannot be null"));

            client.DefaultRequestHeaders.Add("removed", "removed");

        }).ConfigurePrimaryHttpMessageHandler(() =>
        {
            HttpClientHandler clientHandler = new HttpClientHandler
            {
                ClientCertificateOptions = ClientCertificateOption.Manual,
                ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
            };
            clientHandler.ClientCertificates.Add(new X509Certificate2(securitySession.CertificateData.Data, securitySession.CertificateData.Password));
            return new HttpClientXRayTracingHandler(clientHandler);
        })
        .AddPolicyHandler(HttpPolicies.GetRetryPolicy())
        .SetHandlerLifetime(TimeSpan.FromSeconds(1));
    }
}

And use it like this:

services.AddCustomHttpClient<IUserService, UserService>(securitySession);
services.AddCustomHttpClient<IAccountService, AccountService>(securitySession);
services.AddCustomHttpClient<ICustomerService, CustomerService>(securitySession);

CodePudding user response:

if you want the same httpclient to be reused then create one like

services.AddHttpClient<IHTTPservice, HTTPservice>(client =>
{
client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("EXTERNAL_SERVICE_BASE_URL") ??
                             throw new 
ArgumentNullException("EXTERNAL_SERVICE_BASE_URLcannot be null"));

client.DefaultRequestHeaders.Add("removed", "removed");

}).ConfigurePrimaryHttpMessageHandler(() =>
{
HttpClientHandler clientHandler = new HttpClientHandler
{
    ClientCertificateOptions = ClientCertificateOption.Manual,
    ServerCertificateCustomValidationCallback = (sender, cert, chain, 
 sslPolicyErrors) => true
};
clientHandler.ClientCertificates.Add(new 
X509Certificate2(securitySession.CertificateData.Data, 
securitySession.CertificateData.Password));
return new HttpClientXRayTracingHandler(clientHandler);
})
.AddPolicyHandler(HttpPolicies.GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromSeconds(1));

and define is as for example

public interface IHTTPservice
{
Task<T> Get<T>(string uri, int timeOut = 100, CancellationToken token = default);
Task<T> Post<T>(string uri, HttpContent content, int timeOut = 100, CancellationToken token = default);
Task<T> Post<T>(string uri, object value, int timeOut = 100, CancellationToken token = default);
}

public class HttpService : IHttpService
{
  private readonly HttpClient _httpClient;
 
public HttpService(HttpClient httpClient)
{
    _httpClient = httpClient;
}

 public async Task<T> Get<T>(string uri, int timeOut = 100, CancellationToken token = default)
{
    var request = new HttpRequestMessage(HttpMethod.Get, uri);
    return await sendRequest<T>(request,timeOut, token);
}

public async Task<T> Post<T>(string uri, HttpContent content, int timeOut = 100, CancellationToken token = default)
{
    var request = new HttpRequestMessage(HttpMethod.Post, uri);
    request.Content = content;
    return await sendRequest<T>(request, timeOut, token);
}

public async Task<T> Post<T>(string uri, object value, int timeOut = 100, CancellationToken token = default)
{
    var request = new HttpRequestMessage(HttpMethod.Post, uri);
    request.Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json");
    return await sendRequest<T>(request, timeOut, token);
}

// helper methods

private async Task<T> sendRequest<T>(HttpRequestMessage request, int timeOut = 100, CancellationToken token = default)
{
    try
    {
        using var requestCTS = new CancellationTokenSource(TimeSpan.FromSeconds(timeOut));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestCTS.Token, token);
        using var response = await _httpClient.SendAsync(request, linkedCts.Token);
      
        // throw exception on error response
        if (!response.IsSuccessStatusCode)
        {
            var error = await response.Content.ReadAsStringAsync(token);
            throw new Exception(error);
        }

        return (await response.Content.ReadFromJsonAsync<T>())!;

    }
    catch (AccessTokenNotAvailableException)
    {
        //what you want
    }

and then You may inject this IHTTPservice where You want and use this single generic HttpService in other services.

not sure if it is best practise - please comment of someone known but it works well.

  • Related