About
In this code snippet, I will show you how to properly use the C# Http Client in Azure Functions using dependency injection(the same concept applies to other C# .NET technologies). I’ll also show you how to add request caching to improve performance.
The HttpClient implements the IDisposable interface so you would think you can just create a client use it within a try/catch and then dispose of it or use it with the using statement. However, if you are doing this a lot it might lead to websocket exhaustion as a websocket will not get released immideatly after the client is disposed of.
To fix this you can create an Http Client(preferably by using the HttpClientFactory) and reuse it by dependency injecting it into the functions/methods/modules making the requests.
Another thing you should consider when using an Http Client is implementing software design patterns/mechanisms/concepts such as Retry, Circuit Breaker, Hedging, Timeout, Rate Limiter, and Fallback. This will improve the reliability of your software as Http requests can be flaky a times. I described how to do this using a library called Polly in this post.
Let’s have a look at the code below to see how to work with the HttpClient.
Prerequisites:
using Microsoft.Extensions.Caching.Memory; using System.Security.Cryptography;
Program.cs File
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureServices((context, services) => { //Add memory cache service. services.AddMemoryCache(); //Add http client service. services.AddHttpClient(); //You can also create a named HttpClient and set it up for a specific request. This way you don't have set it up every time in code. //Register named HttpClient as a singleton service. More about named HttpClients: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0#named-and-typed-clients services.AddHttpClient("PlaceholderJsonClient", client => { //Set the url the client will call. client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/todos/1"); //Set the default request headers that will be added to every request. //See the code extra example code to see how to set one-time headers for a specific request. client.DefaultRequestHeaders.Add("Content-Type", "text/plain; charset=utf-8"); }); }) .Build(); host.Run();
Code
using System.Net; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Caching.Memory; using System.Text; using System.Security.Cryptography; namespace DemoFunctionApp { public class Function1 { private readonly ILogger _logger; private readonly HttpClient _client; private readonly IMemoryCache _cache; private readonly SHA256 sha256Hash; //Constructor that injects the logger and the HttpClient and the Cache into the instance. public Function1(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory, IMemoryCache cache) { _logger = loggerFactory.CreateLogger<Function1>(); _client = httpClientFactory.CreateClient(); //Use the named client we registered. //_client = httpClientFactory.CreateClient("PlaceholderJsonClient"); _cache = cache; sha256Hash = SHA256.Create(); } //This is just an example/demo Azure Functions HTTP endpoint we'll call to run our code. [Function("Function1")] public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) { //Inject the client into the request making function instead of creating it every time. //We'll do the same with the cache, and hashing function. string message = await MakeRequestToTestAPI(_client, _cache, sha256Hash); //Create and return a response. var response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); response.WriteString(message); return response; } private async Task<string> MakeRequestToTestAPI(HttpClient client, IMemoryCache cache, SHA256 sha256Hash) { //Here I'll just hardcode the URL and the request body for the sake of the example. string url = "https://jsonplaceholder.typicode.com/posts"; string requestBody = "{\"title\": \"foo\", \"body\": \"barr\", \"userId\": 1}"; //Create a key for the cache from the URL and body. If your request doesn't have a body(GET) you can just use the URL with its parameters. //Or you can even just use only certain parameters. string key = url + requestBody; //Reduce the key to a hash. byte[] keyHash = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(requestBody)); //Check if the response is already cached. If so return it. string cachedValue = cache.Get<string>(keyHash); if (cachedValue is not null) return cachedValue; //Else make the request ... //I'm using the PostAsync method here but you can use any other method you need. var response = await client.PostAsync(url, new StringContent(requestBody, Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); string responseContent = await response.Content.ReadAsStringAsync(); #region Separately defined request with one-time headers //////////////////////////////////////// //This is just an example of a different way to make a request(with added one time-headers). HttpRequestMessage request = new HttpRequestMessage() { RequestUri = new Uri(url), Method = HttpMethod.Post, Content = new StringContent(requestBody, Encoding.UTF8, "application/json"), //This is how you can set one-time headers for this specific request. //Unlike the default headers that are set on the client and are added to every request. Headers = { { "Content-Type", "application/json" } } }; var response2 = await client.SendAsync(request); response2.EnsureSuccessStatusCode(); string responseContent2 = await response2.Content.ReadAsStringAsync(); #endregion //////////////////////////////////////////////////////////////////////////////////// //... cache the response and return it. cache.Set(keyHash, responseContent, TimeSpan.FromMinutes(60)); return responseContent; } } }