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

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

According to the documentation and several articles, the class is best created once for the life of the application and used for several requests. Unfortunately, I could not find an example of how to do this correctly in Core, so Ive come up with the following solution.

For my specific needs, using two different endpoints (I have an APIServer for business logic and an ImageServer managed API), so I think I have 2 HttpClient singlets that I can use in the application.

Ive configured my service points in appsettings.json as follows:

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

Then I created an HttpClientsFactory that will instantiate my 2 clients and store 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 an interface that I can use when defining my DI later. Note that the HttpClientsFactory class inherits from this interface.

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

Now I am ready to insert this into my Dependency container, as described in the Startup class in the ConfigureServices method.

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

Now everything is set up to start using this in my controller.
First, I take the addiction. To do this, I created a private class property to store it, and then add it to the constructor signature and finish by assigning the incoming object to the local property of the class.

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

Finally, we can now use Factory to query htppclient and make the call. The following is an example when I request an image from an image server using 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!

Ive checked this and it works great in my development environment. However, I'm not sure if this is the best way to implement this. I remain with the following questions:

  • Is this stream of solutions safe? (according to MS doc: "All public static (Shared in Visual Basic) members of this type are thread safe.)
  • Will this setting work under heavy load without opening many separate connections?
  • What to do in the ASP.Net kernel to handle the DNS problem described in "Singleton HttpClient"? Beware of this serious behavior and how to fix it. located at http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  • Any other improvements or suggestions?
+9
c # asp.net-core asp.net-core-mvc asp.net-core-webapi
source share
2 answers

In response to a question from @MuqeetKhan regarding the use of authentication with an httpclient request.

Firstly, my motivation for using DI and factory was to allow me to easily extend my application to different and multiple APIs and have easy access to it throughout my code. His template, I hope, will be able to reuse it.

In the case of my GetUserPicture controller described in the original question above, I really removed authentication for reasons of simplicity. Honestly, however, I still doubt that I need it there just to get the image from the image server. In any case, in other controllers I really need this, so ...

Ive implemented Identityserver4 as my authentication server. This gives me authentication over ASP Identity. For authorization (using roles in this case), I implemented the IClaimsTransformer project in MVC and API projects (you can read more about this here in How to put Identity Server Server roles in Identityserver4 Identity Identifier ).

Now, when I log in to my controller, I have an authenticated and authorized user for whom I can get an access token. I use this token to call my api, which, of course, calls the same instance of the identity server to authenticate the user.

The final step is to let my API check if the user is allowed to call the requested api controller. In the API request pipeline using IClaimsTransformer, as explained earlier, I get the caller’s authorization and add it to the incoming requests. Please note that in case of calling MVC and API, I thus get authorization 2 times; once in the MVC request pipeline and once in the API request pipeline.

Using this setting, I can use my HttpClientsFactory with authorization and authentication.

On most of the security that I skip, there are HTTPS of course. Hope I can somehow add it to my factory. I will update it as soon as I implemented it.

As always, any suggestions are welcome.

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

My MVC controller calling "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 loading user load

  [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); } 
+1
source

If you HttpClientFactory core 2.1 or higher, the best approach would be to use the new HttpClientFactory . I assume that Microsoft was aware of all the problems that people faced, so they did the hard work for us. See below how to configure it.

NOTE. Add a link 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 customers 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 you need at startup using services.AddHttpClient

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

0
source

All Articles