I have an ASP.NET MVC application which invokes an ASP.NET Web API REST Service each time a button is pressed in the UI.

Each time this button is pressed below DumpWarehouseDataIntoFile method is executed.

public class MyClass
{
    private static HttpClient client = new HttpClient();

    public async Task DumpWarehouseDataIntoFile(Warehouse myData, string path, string filename)
    { 
        try
        {
           //Hosted web API REST Service base url  
           string Baseurl = "http://XXX.XXX.XX.X:YYYY/";  

           //using (var client = new HttpClient())  --> I have declared client as an static variable
           //{  
            //Passing service base url  
            client.BaseAddress = new Uri(Baseurl);  

            client.DefaultRequestHeaders.Clear();  

            //Define request data format  
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));  
              
            // Serialize parameter to pass to the asp web api rest service
            string jsonParam = Newtonsoft.JsonConvert.SerializeObject(myData);

            //Sending request to find web api REST service resource  using HttpClient
            var httpContent = new StringContent(jsonParam, Encoding.UTF8, "application/json");

            HttpResponseMessage Res = await client.PostAsync("api/Warehouse/DumpIntoFile", httpContent);  

            //Checking the response is successful or not which is sent using HttpClient  
            if (Res.IsSuccessStatusCode)  
            {  
               // Some other sftuff here
            }
           //}
         }
         catch (Exception ex)
         {
              // Do some stuff here
         } // End Try

    } // End DumpWarehouseDataIntoFile method
} // End class

Warehouse class object:

public class Warehouse
{
    public DataTable dt { get; set; }
    public string Filepath { get; set; }
}

I have found in this post that pattern:

using (var myClient = new HttpClient())
{
}

is not recommended to be used since it leads to socket exhaustion (System.Net.Sockets.SocketException). There it is recommended to use HttpClient as static variable and reuse it as it helps to reduce waste of sockets. So I have used a static variable.

The problem with this approach (in my scenario) is that it only works first button is pressed, next times button is pressed and DumpWarehouseDataIntoFile method is executed, below exception is thrown:

An unhandled exception has occurred while executing the request. System.InvalidOperationException: This instance has already started one or more requests. Properties can only be modified before sending the first request.

As error says, properties like base address, etc. can only be modified once before sending the first request.

I have googled and found some solutions proposed:

First solution

So it seems like singleton pattern would be a good option, as proposed here. Below the singleton proposed by Alper:

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

Usage

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

The problem I see here is that if you call above code many times (in my case would be each time I press the button on the UI and I call DumpWarehouseDataIntoFile method), you create and instance of MyApiClient each time and therefore a new instance of HttpClient is created and I want to reuse HttpClient, not to make many instances of it.

Second solution

Creating a kind of factory as proposed here by Nico. Below the code he proposes:

public interface IHttpClientFactory
{
    HttpClient CreateClient();
}

public class HttpClientFactory : IHttpClientFactory
{
    static string baseAddress = "http://example.com";

    public HttpClient CreateClient()
    {
        var client = new HttpClient();
        SetupClientDefaults(client);
        return client;
    }

    protected virtual void SetupClientDefaults(HttpClient client)
    {
        client.Timeout = TimeSpan.FromSeconds(30); //set your own timeout.
        client.BaseAddress = new Uri(baseAddress);
    }
}

Usage

public HomeController(IHttpClientFactory httpClientFactory)
{
    _httpClientFactory = httpClientFactory;
}

readonly IHttpClientFactory _httpClientFactory;

public IActionResult Index()
{
    var client = _httpClientFactory.CreateClient();
    //....do your code
    return View();
}

Here again you create a new instance of HttpClient each time you call CreateClient. You do not reuse HttpClient object.

Third Solution

Making HTTP requests using IHttpClientFactory as explained here. The problem is that it is only available for .NET Core, not standard ASP.NET Framework, though it seems it is available by installing this nuget package. It seems like it automatically manages efficiently HttpClient instances and I would like to apply it to my scenario. I want to avoid to reinvent the wheel.

I have never used IHttpClientFactory and I have no idea on how to use it: configure some features like base address, set request headers, create an instance of HttpClient and then invoke PostAsync on it passing as parameter the HttpContent.

I think this is the best approach so could someone tell me the necessary steps I need to do in order to make the same things I do in DumpWarehouseDataIntoFile method but using IHttpClientFactory? I am a bit lost, I do not know how to apply IHttpClientFactory to do the same as I do within DumpWarehouseDataIntoFile method.

Any others solutions not proposed here and also some code snippets will be highly appreciated.

3

There are 3 answers

0
Peter Csala On

HttpClient

The HttpClient can throw InvalidOperationException in the following cases:

  • When the BaseAddress setter is called after a request has been sent out
  • When the Timeout setter is called after a request has been sent out
  • When the MaxResponseContentBufferSize setter is called after a request has been sent out
  • When an operation has already started and resend was requested

In order to avoid these you can set the first two on per request level, for example:

CancellationTokenSource timeoutSource = new CancellationTokenSource(2000);
await httpClient.GetAsync("http://www.foo.bar", timeoutSource.Token);

HttpClientFactory

You can use the IHttpClientFactory in .NET Framework with the following trick:

  • AddHttpClient registers the DefaultHttpClientFactory for IHttpClientFactory
  • Then you can retrieve it from the DI container
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider();
container.RegisterInstance(serviceProvider.GetService<IHttpClientFactory>());
container.ContainerScope.RegisterForDisposal(serviceProvider);

This sample uses SimpleInjector but the same concept can be applied for any other DI framework.

0
Daniil Loban On

I'm not sure but will what happen if you move this lines to constructor:

        //Passing service base url  
        client.BaseAddress = new Uri(Baseurl);  

        client.DefaultRequestHeaders.Clear();  

        //Define request data format  
        client.DefaultRequestHeaders.Accept
       .Add(new MediaTypeWithQualityHeaderValue("application/json"));  

I think that re-initialization is problem.

0
Abraham On

Better to add the request url and the headers at the message. Don't use httpClient.BaseAddress or httpClient.DefaultRequestHeaders unless you have a default requirement.

HttpRequestMessage msg = new HttpRequestMessage {
    Method = HttpMethod.Put,
    RequestUri = new Uri(url),
    Headers = httpRequestHeaders;
};

httpClient.SendAsync(msg);

It works well for reusing the HttpClient for many requests