Detailed explanation of HttpClientFactory class for in depth understanding of DotNetCore

  • 2021-11-02 00:37:59
  • OfStack

The HttpClient class is commonly used when you need to send an HTTP request to a particular URL address and get a response. This class contains many useful methods, which can meet most requirements. However, if it is not used properly, unexpected things may occur.


using(var client = new HttpClient())

The resources occupied by the object should be ensured to be released in time, but this is wrong for network connections.

There are 2 reasons. Network connection takes 1 time. Frequent opening and closing of connections will affect performance; Furthermore, when opening the network connection, the underlying socket resources will be occupied, but when HttpClient calls its own Dispose method, the resources cannot be released immediately, which means that your program may cause unexpected exceptions due to exhaustion of connection resources.

Therefore, a better solution is to extend the service life of HttpClient objects, such as building a static object for them:


private static HttpClient Client = new HttpClient();

But from the programmer's point of view, this kind of code may not be elegant enough.

So the new HttpClientFactory class is introduced in. NET Core 2.1.

Its usage is very simple, first of all, it is registered with IoC:


 public void ConfigureServices(IServiceCollection services)
 {
  services.AddHttpClient();
  services.AddMvc();
 }

Then create an HttpClient object through IHttpClientFactory, and the subsequent operation remains the same, but there is no need to worry about the release of its internal resources:


public class LzzDemoController : Controller
{
 IHttpClientFactory _httpClientFactory;

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

 public IActionResult Index()
 {
  var client = _httpClientFactory.CreateClient();
  var result = client.GetStringAsync("http://myurl/");
  return View();
 }
}

AddHttpClient source code:


public static IServiceCollection AddHttpClient(this IServiceCollection services)
{
 if (services == null)
 {
  throw new ArgumentNullException(nameof(services));
 }

 services.AddLogging();
 services.AddOptions();

 //
 // Core abstractions
 //
 services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
 services.TryAddSingleton<IHttpClientFactory, DefaultHttpClientFactory>();

 //
 // Typed Clients
 //
 services.TryAdd(ServiceDescriptor.Singleton(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));

 //
 // Misc infrastructure
 //
 services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());

 return services;
}

Its interior is bound with DefaultHttpClientFactory class for IHttpClientFactory interface.

Look again at the key CreateClient method in the IHttpClientFactory interface:


public HttpClient CreateClient(string name)
{
 if (name == null)
 {
  throw new ArgumentNullException(nameof(name));
 }

 var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
 var client = new HttpClient(entry.Handler, disposeHandler: false);

 StartHandlerEntryTimer(entry);

 var options = _optionsMonitor.Get(name);
 for (var i = 0; i < options.HttpClientActions.Count; i++)
 {
  options.HttpClientActions[i](client);
 }

 return client;
}

The creation of HttpClient is no longer a simple new HttpClient (), but two parameters are passed in: HttpMessageHandler handler and bool disposeHandler. When the disposeHandler parameter is an false value, the internal handler object is to be reused. The handler parameter can be seen from the code in the previous sentence that it is taken out of the 1 dictionary with name as the key value, and because the DefaultHttpClientFactory class is registered by TryAddSingleton method, which means that it is a singleton, then this internal dictionary is only 1, and the ActiveHandlerTrackingEntry object corresponding to each key value is also only 1, which contains handler inside.

The next sentence code StartHandlerEntryTimer (entry); Expiration timing processing of ActiveHandlerTrackingEntry object is turned on. The default expiration time is 2 minutes.


internal void ExpiryTimer_Tick(object state)
{
 var active = (ActiveHandlerTrackingEntry)state;

 // The timer callback should be the only one removing from the active collection. If we can't find
 // our entry in the collection, then this is a bug.
 var removed = _activeHandlers.TryRemove(active.Name, out var found);
 Debug.Assert(removed, "Entry not found. We should always be able to remove the entry");
 Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced");

 // At this point the handler is no longer 'active' and will not be handed out to any new clients.
 // However we haven't dropped our strong reference to the handler, so we can't yet determine if
 // there are still any other outstanding references (we know there is at least one).
 //
 // We use a different state object to track expired handlers. This allows any other thread that acquired
 // the 'active' entry to use it without safety problems.
 var expired = new ExpiredHandlerTrackingEntry(active);
 _expiredHandlers.Enqueue(expired);

 Log.HandlerExpired(_logger, active.Name, active.Lifetime);

 StartCleanupTimer();
}

First, the ActiveHandlerTrackingEntry object is passed into the new ExpiredHandlerTrackingEntry object.


public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
 Name = other.Name;

 _livenessTracker = new WeakReference(other.Handler);
 InnerHandler = other.Handler.InnerHandler;
}

Inside its constructor, handler objects are associated by weak references that do not affect their release by GC.

Then the newly created ExpiredHandlerTrackingEntry object is put into a dedicated queue.

Finally, the cleaning work is started, and the time interval of the timer is set to once every 10 seconds.


internal void CleanupTimer_Tick(object state)
{
 // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup.
 //
 // With the scheme we're using it's possible we could end up with some redundant cleanup operations.
 // This is expected and fine.
 // 
 // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it
 // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out
 // whether we need to start the timer.
 StopCleanupTimer();

 try
 {
  if (!Monitor.TryEnter(_cleanupActiveLock))
  {
   // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes
   // a long time for some reason. Since we're running user code inside Dispose, it's definitely
   // possible.
   //
   // If we end up in that position, just make sure the timer gets started again. It should be cheap
   // to run a 'no-op' cleanup.
   StartCleanupTimer();
   return;
  }

  var initialCount = _expiredHandlers.Count;
  Log.CleanupCycleStart(_logger, initialCount);

  var stopwatch = ValueStopwatch.StartNew();

  var disposedCount = 0;
  for (var i = 0; i < initialCount; i++)
  {
   // Since we're the only one removing from _expired, TryDequeue must always succeed.
   _expiredHandlers.TryDequeue(out var entry);
   Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue");

   if (entry.CanDispose)
   {
    try
    {
     entry.InnerHandler.Dispose();
     disposedCount++;
    }
    catch (Exception ex)
    {
     Log.CleanupItemFailed(_logger, entry.Name, ex);
    }
   }
   else
   {
    // If the entry is still live, put it back in the queue so we can process it 
    // during the next cleanup cycle.
    _expiredHandlers.Enqueue(entry);
   }
  }

  Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count);
 }
 finally
 {
  Monitor.Exit(_cleanupActiveLock);
 }

 // We didn't totally empty the cleanup queue, try again later.
 if (_expiredHandlers.Count > 0)
 {
  StartCleanupTimer();
 }
}

The core of the above method is to judge whether the handler object has been GC, and if so, release its internal resources, that is, network connection.

Going back to the code that originally created HttpClient, you will find that no name parameter value was passed in. This is due to the extension method of HttpClientFactoryExtensions class.


public static HttpClient CreateClient(this IHttpClientFactory factory)
{
 if (factory == null)
 {
  throw new ArgumentNullException(nameof(factory));
 }

 return factory.CreateClient(Options.DefaultName);
}

The value for Options. DefaultName is string. Empty.

DefaultHttpClientFactory lacks a parameterless construction method, and only 1 construction method needs to pass in multiple parameters, which also means that it needs to rely on other 1 classes when building it, so it is only suitable for use in ASP. NET programs at present, and cannot be applied to programs such as console 1. I hope that the official will continue to enhance it later, so that the application scope will become wider.


private static HttpClient Client = new HttpClient();
0

Summarize


Related articles: