Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to use HTTPClient in ASP.Net Core as a DI Singleton

Tags:

I am trying to figure out how to best use the HttpClient class in ASP.Net Core.

According to the documentation and several articles, the class is best instantiated once for the lifetime of the application and shared for multiple requests. Unfortunately, I could not find an example of how to correctly do this in Core so I’ve come up with the following solution.

My particular needs require the use of 2 different endpoints (I have an APIServer for business logic and an API driven ImageServer), so my thinking is to have 2 HttpClient singletons that I can use in the application.

I’ve configured my servicepoints in the appsettings.json as follows:

"ServicePoints": { "APIServer": "http://localhost:5001", "ImageServer": "http://localhost:5002", } 

Next, I created a HttpClientsFactory that will instantiate my 2 httpclients and hold them in a static Dictionary.

public class HttpClientsFactory : IHttpClientsFactory {     public static Dictionary<string, HttpClient> HttpClients { get; set; }     private readonly ILogger _logger;     private readonly IOptions<ServerOptions> _serverOptionsAccessor;      public HttpClientsFactory(ILoggerFactory loggerFactory, IOptions<ServerOptions> serverOptionsAccessor) {         _logger = loggerFactory.CreateLogger<HttpClientsFactory>();         _serverOptionsAccessor = serverOptionsAccessor;         HttpClients = new Dictionary<string, HttpClient>();         Initialize();     }      private void Initialize()     {         HttpClient client = new HttpClient();         // ADD imageServer         var imageServer = _serverOptionsAccessor.Value.ImageServer;         client.BaseAddress = new Uri(imageServer);         HttpClients.Add("imageServer", client);          // ADD apiServer         var apiServer = _serverOptionsAccessor.Value.APIServer;         client.BaseAddress = new Uri(apiServer);         HttpClients.Add("apiServer", client);     }      public Dictionary<string, HttpClient> Clients()     {         return HttpClients;     }      public HttpClient Client(string key)     {         return Clients()[key];     }   }  

Then, I created the interface that I can use when defining my DI later on. Notice that the HttpClientsFactory class inherits from this interface.

public interface IHttpClientsFactory {     Dictionary<string, HttpClient> Clients();     HttpClient Client(string key); } 

Now I am ready to inject this into my Dependency container as follows in the Startup class under the ConfigureServices method.

// Add httpClient service         services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>(); 

All is now set-up to start using this in my controller.
Firstly, I take in the dependency. To do this I created a private class property to hold it, then add it to the constructor signature and finish by assigning the incoming object to the local class property.

private IHttpClientsFactory _httpClientsFactory; public AppUsersAdminController(IHttpClientsFactory httpClientsFactory) {    _httpClientsFactory = httpClientsFactory; } 

Finally, we can now use the Factory to request a htppclient and execute a call. Below, an example where I request an image from the imageserver using the httpclientsfactory:

[HttpGet]     public async Task<ActionResult> GetUserPicture(string imgName)     {         // get imageserver uri         var imageServer = _optionsAccessor.Value.ImageServer;          // create path to requested image         var path = imageServer + "/imageuploads/" + imgName;          var client = _httpClientsFactory.Client("imageServer");         byte[] image = await client.GetByteArrayAsync(path);          return base.File(image, "image/jpeg");     } 

Done!

I’ve tested this and it work great on my development environment. However, I am not sure if this is the best way to implement this. I remain with the following questions:

  1. Is this solution thread safe? (according to the MS doc: ‘Any public static (Shared in Visual Basic) members of this type are thread safe.’)
  2. Will this set-up be able to handle a heavy load without opening many separate connection?
  3. What to do in ASP.Net core to handle the DNS problem described in ‘Singleton HttpClient? Beware of this serious behaviour and how to fix.’ located at http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. Any other improvements or suggestions?
like image 480
Laobu Avatar asked Feb 06 '17 10:02

Laobu


People also ask

Is AddHttpClient a singleton?

Http NuGet package that includes the AddHttpClient extension method for IServiceCollection. This extension method registers the internal DefaultHttpClientFactory class to be used as a singleton for the interface IHttpClientFactory .

Should we create a new single instance of HttpClient for all requests?

The correct way as per the post is to create a single instance of HttpClient as it helps to reduce waste of sockets.

Should I dispose HttpClient from IHttpClientFactory?

Answer when using HttpClientFactory: There is no need to dispose of the HttpClient instances from HttpClientFactory.

Is IHttpClientFactory a singleton?

public class MyService { // IHttpClientFactory is a singleton, so can be injected everywhere private readonly IHttpClientFactory _factory; public MyService(IHttpClientFactory factory) { _factory = factory; } public async Task DoSomething() { // Get an instance of the typed client HttpClient client = _factory.


2 Answers

If using .net core 2.1 or higher, the best approach would be to use the new HttpClientFactory. I guess Microsoft realized all the issues people were having so they did the hard work for us. See below for how to set it up.

NOTE: Add a reference to Microsoft.Extensions.Http.

1 - Add a class that uses HttpClient

public interface ISomeApiClient {     Task<HttpResponseMessage> GetSomethingAsync(string query); }  public class SomeApiClient : ISomeApiClient {     private readonly HttpClient _client;      public SomeApiClient (HttpClient client)     {         _client = client;     }      public async Task<SomeModel> GetSomethingAsync(string query)     {         var response = await _client.GetAsync($"?querystring={query}");         if (response.IsSuccessStatusCode)         {             var model = await response.Content.ReadAsJsonAsync<SomeModel>();             return model;         }         // Handle Error     } } 

2 - Register your clients in ConfigureServices(IServiceCollection services) in Startup.cs

var someApiSettings = Configuration.GetSection("SomeApiSettings").Get<SomeApiSettings>(); //Settings stored in app.config (base url, api key to add to header for all requests) services.AddHttpClient<ISomeApiClient, SomeApiClient>("SomeApi",                 client =>                 {                     client.BaseAddress = new Uri(someApiSettings.BaseAddress);                     client.DefaultRequestHeaders.Add("api-key", someApiSettings.ApiKey);                 }); 

3 - Use the client in your code

public class MyController {     private readonly ISomeApiClient _client;      public MyController(ISomeApiClient client)     {         _client = client;     }      [HttpGet]     public async Task<IActionResult> GetAsync(string query)     {         var response = await _client.GetSomethingAsync(query);          // Do something with response          return Ok();     } } 

You can add as many clients and register as many as needed in your startup with services.AddHttpClient

Thanks to Steve Gordon and his post here for helping me use this in my code!

like image 92
garethb Avatar answered Oct 09 '22 12:10

garethb


In answer to a question from @MuqeetKhan regarding using authentication with the httpclient request.

Firstly, my motivation to use DI and a factory was to allow me to extend my application easily to different and multiple API’s and have easy access to that throughout my code. It’s a template I hope to be able to reuse multiple times.

In the case of my ‘GetUserPicture’ controller decribed in the original question above, I indeed for simplicity reasons removed the authentication. Honestly however, I am still in doubt if I need it there to simply retrieve an image from the imageserver. Anyhow, in other controllers I definitely do need it, so…

I’ve implemented Identityserver4 as my authentication server. This provides me with the authentication on top of ASP Identity. For authorization (using roles in this case), I implemented IClaimsTransformer in my MVC ‘and’ API projects (you can read more about this here at How to put ASP.net Identity Roles into the Identityserver4 Identity token).

Now, the moment I enter my controller I have an authenticated and authorized user for which I can retrieve an access token. I use this token to call my api which is of course calling the same instance of identityserver to verify if the user is authenticated.

The last step is to allow my API to verify if the user is authorized to call the requested api controller. In the request pipeline of the API using IClaimsTransformer as explained before, I retrieve the authorization of the calling user and add it to the incoming claims. Note that in case of an MVC calling and API, I thus retrieve the authorization 2 times; once in the MVC request pipeline and once in the API request pipeline.

Using this set-up I am able to use my HttpClientsFactory with Authorization and Authentication.

On big security part I am missing is HTTPS of course. I hope I can somehow add it to my factory. I'll update it once I've implemented it.

As always, any suggestions are welcome.

Below an example where I upload an image to the Imageserver using authentication (user must be logged in and have role admin).

My MVC controller calling the ‘UploadUserPicture’:

    [Authorize(Roles = "Admin")]     [HttpPost]     public async Task<ActionResult> UploadUserPicture()     {         // collect name image server         var imageServer = _optionsAccessor.Value.ImageServer;          // collect image in Request Form from Slim Image Cropper plugin         var json = _httpContextAccessor.HttpContext.Request.Form["slim[]"];          // Collect access token to be able to call API         var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");          // prepare api call to update image on imageserver and update database         var client = _httpClientsFactory.Client("imageServer");         client.DefaultRequestHeaders.Accept.Clear();         client.SetBearerToken(accessToken);         var content = new FormUrlEncodedContent(new[]         {             new KeyValuePair<string, string>("image", json[0])         });         HttpResponseMessage response = await client.PostAsync("api/UserPicture/UploadUserPicture", content);          if (response.StatusCode != HttpStatusCode.OK)         {             return StatusCode((int)HttpStatusCode.InternalServerError);         }         return StatusCode((int)HttpStatusCode.OK);     } 

API handling the user upload

    [Authorize(Roles = "Admin")]     [HttpPost]     public ActionResult UploadUserPicture(String image)     {      dynamic jsonDe = JsonConvert.DeserializeObject(image);          if (jsonDe == null)         {             return new StatusCodeResult((int)HttpStatusCode.NotModified);         }          // create filname for user picture         string userId = jsonDe.meta.userid;         string userHash = Hashing.GetHashString(userId);         string fileName = "User" + userHash + ".jpg";          // create a new version number         string pictureVersion = DateTime.Now.ToString("yyyyMMddHHmmss");          // get the image bytes and create a memory stream         var imagebase64 = jsonDe.output.image;         var cleanBase64 = Regex.Replace(imagebase64.ToString(), @"^data:image/\w+;base64,", "");         var bytes = Convert.FromBase64String(cleanBase64);         var memoryStream = new MemoryStream(bytes);          // save the image to the folder         var fileSavePath = Path.Combine(_env.WebRootPath + ("/imageuploads"), fileName);         FileStream file = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write);         try         {             memoryStream.WriteTo(file);         }         catch (Exception ex)         {             _logger.LogDebug(LoggingEvents.UPDATE_ITEM, ex, "Could not write file >{fileSavePath}< to server", fileSavePath);             return new StatusCodeResult((int)HttpStatusCode.NotModified);         }         memoryStream.Dispose();         file.Dispose();         memoryStream = null;         file = null;          // update database with latest filename and version         bool isUpdatedInDatabase = UpdateDatabaseUserPicture(userId, fileName, pictureVersion).Result;          if (!isUpdatedInDatabase)         {             return new StatusCodeResult((int)HttpStatusCode.NotModified);         }          return new StatusCodeResult((int)HttpStatusCode.OK);     } 
like image 26
Laobu Avatar answered Oct 09 '22 14:10

Laobu