3

This is a made up scenario and I want to find the best way of implementing and learn which are the best practices.

I am writing a service that makes a get request to an api which requires an Authorization header, the problem is that I am initializing a new client for each service, I was thinking if it is the best to have a singleton client and use that anywhere in different services.

Here I have my BlogService, where I initialize the client and then add the Authorization header, username and password will be in configs but this is just an example.

namespace MyDemoProject.Services
{
  public class BlogService : IBlogService
  {
    private readonly HttpClient client;
    private string username = "myUsername";
    private string password = "myPassword";
    private static string url = "https://example.com/api/blogs";

    public BlogService()
    {
      client = new HttpClient();
      string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));
      client.DefaultRequestHeaders.Add("Authorization", "Basic " + encoded);
    }
    public async Task<string> GetAllBlogs()
    {
      var request = await client.GetAsync(url);
      var data = await request.Content.ReadAsStringAsync();
      return data;
    }
  }
}

And here I have my CommentService, which is very similar, but with a different url. This is a simple example and it could be included in the same BlogService, but suppose that I want to make it granular for maintainability and scalability.

namespace MyDemoProject.Services
{
  public class CommentService : ICommentService
  {
    private readonly HttpClient client;
    private string username = "myUsername";
    private string password = "myPassword";
    private static string url = "https://example.com/api/comments";

    public CommentService()
    {
      client = new HttpClient();
      string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));
      client.DefaultRequestHeaders.Add("Authorization", "Basic " + encoded);
    }
    public async Task<string> GetAllComments()
    {
      var request = await client.GetAsync(url);
      var data = await request.Content.ReadAsStringAsync();
      return data;
    }
  }
}

Is there any better way of implementing this, and are there any issues with this implementation?

Sachihiro
  • 1,597
  • 2
  • 20
  • 46
  • Never new them up like this. See the docs on how to inject HttpClient https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0 – DavidG Jul 19 '22 at 13:15
  • While learning to use HttpClient is good - have you considered using another library such as [Flurl](https://flurl.dev/)? It takes care of the Basic authorization headers for you, and will also handle serialization to/from JSON. This request could be as easy as `List comments = await "https://example.com/api/comments".WithBasicAuth(username, password).GetJsonAsync>();` – mason Jul 19 '22 at 13:38
  • I'd recommend using typed clients instead of using `HttpClientFactory`. Here's an example that adds authentication headers. https://github.com/mysteryx93/OntraportApi.NET/blob/master/OntraportApi/OntraportHttpClient.cs – Etienne Charland Jul 19 '22 at 13:49
  • Also for extra resiliency, you need to configure your client with Polly. Here's a sample registration of your typed client. https://github.com/mysteryx93/OntraportApi.NET#adding-polly= – Etienne Charland Jul 19 '22 at 13:52
  • @EtienneCharland is there any reason to not use HttpClientFactory? – Sachihiro Jul 19 '22 at 13:52
  • If you need a fixed connection for your class, why use the factory when the DI container provides you the IHttpClient directly? What benefit is there to using HttpClientFactory? Typed clients do have clear benefits that you can encapsulate the connection logic within it. I guess Microsoft edited their documentation to prefer HttpClientFactory because people were misusing HttpClient. Ah... one issue with HttpClient injection is handling DNS changes. – Etienne Charland Jul 19 '22 at 13:55
  • @EtienneCharland not familiar with this concept, but thanks for the info. Much appreciated – Sachihiro Jul 19 '22 at 14:00
  • `services.AddHttpClient();` is declaring IHttpClient for DI injection using HttpClientFactory according to the doc so there are no issues there. – Etienne Charland Jul 19 '22 at 14:01
  • BTW I do not agree with the Close because the linked question is not the same. – Etienne Charland Jul 19 '22 at 14:03
  • @EtienneCharland please vote to reopen then. – Sachihiro Jul 19 '22 at 14:05
  • I was writing a response when it got closed. I do not have the rights to vote to re-open. Although I'm sure the question has been answered correctly elsewhere, the current answers aren't satisfactory IMO and thus it would be good to add clarifications. – Etienne Charland Jul 20 '22 at 03:06
  • hah I need just 200 more reputation points to have the right to vote for close or re-open! – Etienne Charland Jul 20 '22 at 03:11

2 Answers2

0

The best practice in .net 6 is to use IHttpClientFactory to avoid the creation of new HttpClient each time you have to use it and to avoid code duplication: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests.

Here a simple example based on your code. You have to register and configure IHttpClientFactory like:

_ = services.AddHttpClient("MyHttpClientKey", (serviceProvider, httpClient) =>
{
   private string username = "myUsername";
   private string password = "myPassword";
   string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));

   _ = httpClient.BaseAddress = new Uri("https://example.com/api/comments");
   _ = httpClient.Timeout = TimeSpan.FromSeconds(30);
   httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + encoded);
});

Then you can use HttpClient injecting it in your classes:

public class CommentService : ICommentService
{
    private readonly HttpClient client;

    public CommentService(IHttpClientFactory httpClientFactory)
    {
        httpClient = httpClientFactory.CreateClient("MyHttpClientKey");
    }

    public async Task<string> GetAllComments()
    {
      var request = await client.GetAsync(url);
      var data = await request.Content.ReadAsStringAsync();
      return data;
    }
}

Hope it helps!

ale91
  • 316
  • 3
  • 8
0

You should not try to manually create instance of HttpClient better avoid it as it might cause unwanted issues due to DNS cache and not recycling of TCP connections.

Use HttpClientFactory instead, it will take care of creating and maintaining the HttpClient for you

Add this dependency from nuget Microsoft.Extensions.DependencyInjection

If you are using dependency injection add this code in startup file
In Startup.cs
  public void ConfigureServices(IServiceCollection services)
        {
services.AddScoped<BearerTokenHandler>();
services.AddHttpClient<ICommentService, CommentService >()
 .ConfigureHttpClient(client =>
                {
                    client.BaseAddress = new Uri("https://example.com/api");
                }).AddHttpMessageHandler<BearerTokenHandler>();;
}


 namespace MyDemoProject.Services
{

public class BearerTokenHandler : DelegatingHandler
    {
        private const string BasicAuthenticationScheme = "Basic";
 private string username = "myUsername";
    private string password = "myPassword";
        public BearerTokenHandler()
        {
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));

            request.Headers.Authorization = new AuthenticationHeaderValue(BasicAuthenticationScheme, token);

            return await base.SendAsync(request, cancellationToken);
        }
    }

  public class CommentService : ICommentService
  {
    private readonly HttpClient client;

    public CommentService(HttpClient httpClient)
    {
client = httpclient
      }
    public async Task<string> GetAllComments()
    {
string response = null;
                    using (var request = new HttpRequestMessage( HttpMethod.GET, "comments"))
                    {
                response = await client.SendAsync(request);

          }
      var data = await response.Content.ReadAsStringAsync();
  
      return data;
    }
  }
}
Sarva Raghavan
  • 88
  • 1
  • 1
  • 11