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:
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 .
The correct way as per the post is to create a single instance of HttpClient as it helps to reduce waste of sockets.
Answer when using HttpClientFactory: There is no need to dispose of the HttpClient instances from HttpClientFactory.
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.
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!
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); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With