Today, we take a deep dive into IHttpClientFactory.
I. Introduction
We first came into contact with it in Dotnet Framework HttpClient
.
HttpClient
Provides us with HTTP
a basic way to interact. But this HttpClient
will throw us two big pits when it is used frequently. On the one hand, if we frequently create and release HttpClient
instances, it will cause the Socket
socket resources to be exhausted because Socket
of the TIME_WAIT
time after closing . This question does not expand, if you need to check TCP
the life cycle. On the other hand, if we create a HttpClient
singleton that is accessed when HTTP
the DNS
recording is changed, it will throw an exception, because HttpClient
does not allow such a change.
Now, for this content, there is a better solution.
From the beginning Dotnet Core 2.1, the framework provides a new content: IHttpClientFactory
.
IHttpClientFactory
Used to create HTTP
interactive HttpClient
instances. It solves the two problems mentioned above by separating HttpClient
the management and the HttpMessageHandler
chain used to send content . Here, the important thing is to manage HttpClientHandler
the life cycle of the pipeline terminal , and this is the actual connection handler.
In addition, IHttpClientFactory
it can also be used IHttpClientBuilder
to easily customize HttpClient
and content processing pipeline, create by pre-configuration HttpClient
, or implemented as base address is provided to add HTTP
class operation.
Let's look at a simple example:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("WangPlus", c =>
{
c.BaseAddress = new Uri("https://github.com/humornif");
})
.ConfigureHttpClient(c =>
{
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
}
In this example, when you call ConfigureHttpClient()
or AddHttpMessageHandler()
to configure HttpClient
, you are actually adding configuration to IOptions
the instance HttpClientFactoryOptions
. This method provides a lot of configuration options, you can go to the Microsoft documentation for details, not much to say here.
In class IHttpClientFactory
, the same way are: creating a IHttpClientFactory
singleton instance, then call CreateClient(name)
creates a name WangPlus
of HttpClient
.
Look at the following example:
public class MyService
{
private readonly IHttpClientFactory _factory;
public MyService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task DoSomething()
{
HttpClient client = _factory.CreateClient("WangPlus");
}
}
The usage is very simple.
Below, we will CreateClient()
conduct an analysis to understand IHttpClientFactory
the content behind.
Second, the creation process of HttpClient & HttpMessageHandler
CreateClient()
Method is IHttpClientFactory
the main method of interaction.
Take a look at CreateClient()
the code implementation:
private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor
public HttpClient CreateClient(string name)
{
HttpMessageHandler handler = CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);
HttpClientFactoryOptions options = _optionsMonitor.Get(name);
for (int i = 0; i < options.HttpClientActions.Count; i++)
{
options.HttpClientActions[i](client);
}
return client;
}
The code looks very simple. First, CreateHandler()
create a HttpMessageHandler
processing pipeline and pass in HttpClient
the name to be created .
With this processing pipeline, it can be created HttpClient
and passed to the processing pipeline. It should be noted here that disposeHandler:false
this parameter is used to ensure that when we release HttpClient
, the processing management will not be released, because IHttpClientFactory
the processing of this pipeline will be completed by itself.
Then, IOptionsMonitor
get the named client from the instance HttpClientFactoryOptions
. It comes from Startup.ConfigureServices()
the HttpClient
configuration function added in and the BaseAddress
sum and Header
other content are set.
Finally, it will be HttpClient
returned to the caller.
After understanding this content, let's take a look at the CreateHandler(name)
method and study how the HttpMessageHandler
pipeline is created.
readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;;
readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory = (name) =>
{
return new Lazy<ActiveHandlerTrackingEntry>(() =>
{
return CreateHandlerEntry(name);
}, LazyThreadSafetyMode.ExecutionAndPublication);
};
public HttpMessageHandler CreateHandler(string name)
{
ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
entry.StartExpiryTimer(_expiryCallback);
return entry.Handler;
}
Look at this code: CreateHandler()
do two things:
Create or acquire
ActiveHandlerTrackingEntry
;Start a timer.
_activeHandlers
It is one ConcurrentDictionary<>
, and HttpClient
the name stored in it (for example, in the code above WangPlus
). The use here Lazy<>
is a GetOrAdd()
trick to keep the method thread safe. The actual creation of the processing pipeline is CreateHandlerEntry
in, it creates one ActiveHandlerTrackingEntry
.
ActiveHandlerTrackingEntry
It is an immutable object, contained HttpMessageHandler
and IServiceScope
injected. In addition, it also contains an StartExpiryTimer()
internal timer used with it to call the callback function when the timer expires.
Look at ActiveHandlerTrackingEntry
the definition:
internal class ActiveHandlerTrackingEntry
{
public LifetimeTrackingHttpMessageHandler Handler { get; private set; }
public TimeSpan Lifetime { get; }
public string Name { get; }
public IServiceScope Scope { get; }
public void StartExpiryTimer(TimerCallback callback)
{
// Starts the internal timer
// Executes the callback after Lifetime has expired.
// If the timer has already started, is noop
}
}
So the CreateHandler
method either creates a new one ActiveHandlerTrackingEntry
, or retrieves the entry from the dictionary, and then starts the timer.
In the next section, let's take a look at how CreateHandlerEntry()
methods create ActiveHandlerTrackingEntry
instances.
Three, create and track HttpMessageHandler in CreateHandlerEntry
CreateHandlerEntry
The method is to create HttpClient
a place for processing pipelines.
This part of the code is a bit complicated, let’s simplify it and focus on the research process:
private readonly IServiceProvider _services;
private readonly IHttpMessageHandlerBuilderFilter[] _filters;
private ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
IServiceScope scope = _services.CreateScope();
IServiceProvider services = scope.ServiceProvider;
HttpClientFactoryOptions options = _optionsMonitor.Get(name);
HttpMessageHandlerBuilder builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
builder.Name = name;
Action<HttpMessageHandlerBuilder> configure = Configure;
for (int i = _filters.Length - 1; i >= 0; i--)
{
configure = _filters[i].Configure(configure);
}
configure(builder);
var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());
return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);
void Configure(HttpMessageHandlerBuilder b)
{
for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
{
options.HttpMessageHandlerBuilderActions[i](b);
}
}
}
Create a container with root DI IServiceScope
, from the associated IServiceProvider
access to services associated, and then from HttpClientFactoryOptions
find the corresponding names HttpClient
and its configuration.
The next item to look up from the container is HttpMessageHandlerBuilder
that the default value is DefaultHttpMessageHandlerBuilder
that this value builds a processing pipeline by creating a main handler (responsible for establishing Socket
sockets and sending requests HttpClientHandler
). We can wrap this main handler by adding additional delegates to create custom management for requests and responses.
The additional delegation is DelegatingHandlers
similar to Core's middleware pipeline:
Configure()
Startup.ConfigureServices()
Build theDelegatingHandlers
pipeline according to the provided configuration ;IHttpMessageHandlerBuilderFilter
It isIHttpClientFactory
a filter injected into the constructor to add additional handlers to the delegated processing pipeline.
IHttpMessageHandlerBuilderFilter
Similar to the one IStartupFilters
registered by default LoggingHttpMessageHandlerBuilderFilter
. This filter adds two additional handlers to the delegate pipeline:
At the beginning of the pipeline
LoggingScopeHttpMessageHandler
, a new log will be startedScope
;At the end of the pipeline
LoggingHttpMessageHandler
, before the request is sent to the masterHttpClientHandler
, log the request and response;
Finally, the entire pipeline is packed in one LifetimeTrackingHttpMessageHandler
. After the pipeline is processed, it will IServiceScope
be saved in a new ActiveHandlerTrackingEntry
instance along with the one used to create it , and given HttpClientFactoryOptions
the lifetime defined in (the default is two minutes).
The entry is returned to the caller ( CreateHandler()
method), added to the handler ConcurrentDictionary<>
, added to the new HttpClient
instance (in the CreateClient()
method), and returned to the original caller.
In the next lifetime (two minutes), whenever you call CreateClient()
, you will get a new HttpClient
instance, but it has the same handler pipeline as when it was originally created.
Each named or typed HttpClient
has its own message handler pipeline. For example, the name WangPlus
two HttpClient
instances will have the same handler chain, but the name api
of HttpClient
the program chains have different processing.
In the next section, we will study the cleaning process after the timer expires.
Three, expired cleanup
With the default time, after two minutes, ActiveHandlerTrackingEntry
the timer stored in will expire and StartExpiryTimer()
the callback method will be triggered ExpiryTimer_Tick()
.
ExpiryTimer_Tick
Responsible for ConcurrentDictionary<>
deleting the handler record from the pool and adding it to the expired handler queue:
readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;
internal void ExpiryTimer_Tick(object state)
{
var active = (ActiveHandlerTrackingEntry)state;
_activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry> found);
var expired = new ExpiredHandlerTrackingEntry(active);
_expiredHandlers.Enqueue(expired);
StartCleanupTimer();
}
When a handler _activeHandlers
is deleted from the collection, when it is called CreateClient()
, it will no longer HttpClient
be distributed with the new one , but will remain in memory until all HttpClient
instances referencing this handler are cleared, IHttpClientFactory
the handler will finally be released Program pipeline.
IHttpClientFactory
Use LifetimeTrackingHttpMessageHandler
and ExpiredHandlerTrackingEntry
to track whether the handler is no longer referenced.
Look at the following code:
internal class ExpiredHandlerTrackingEntry
{
private readonly WeakReference _livenessTracker;
public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name;
Scope = other.Scope;
_livenessTracker = new WeakReference(other.Handler);
InnerHandler = other.Handler.InnerHandler;
}
public bool CanDispose => !_livenessTracker.IsAlive;
public HttpMessageHandler InnerHandler { get; }
public string Name { get; }
public IServiceScope Scope { get; }
}
According to this code, a weak reference to ExpiredHandlerTrackingEntry
the pair LifetimeTrackingHttpMessageHandler
is created . According to what was written in the previous section, it LifetimeTrackingHttpMessageHandler
is the "outermost" handler in the pipeline, so it is a HttpClient
directly referenced handler.
To LifetimeTrackingHttpMessageHandler
use WeakReference
means that the direct reference to the outermost handler in the pipeline is only in HttpClient
. Once the garbage collector has collected all of these HttpClient
, there LifetimeTrackingHttpMessageHandler
will be no references and therefore will be released. ExpiredHandlerTrackingEntry
Can be WeakReference.IsAlive
detected by.
After adding a record to the _expiredHandlers
queue, StartCleanupTimer()
a timer will be started, which will fire after 10 seconds. The CleanupTimer_Tick()
method is called after it is triggered to check whether all references to the handler have expired. If it is, the handler and IServiceScope
will be released. If not, they are added back to the queue and the cleanup timer starts again:
internal void CleanupTimer_Tick()
{
StopCleanupTimer();
int initialCount = _expiredHandlers.Count;
for (int i = 0; i < initialCount; i++)
{
_expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry entry);
if (entry.CanDispose)
{
try
{
entry.InnerHandler.Dispose();
entry.Scope?.Dispose();
}
catch (Exception ex)
{
}
}
else
{
_expiredHandlers.Enqueue(entry);
}
}
if (_expiredHandlers.Count > 0)
{
StartCleanupTimer();
}
}
In order to see the flow of the code, I made this code simple. There are also log records and thread lock related content in the original code.
This method is relatively simple: traverse the ExpiredHandlerTrackingEntry
records and check whether LifetimeTrackingHttpMessageHandler
all references to the handler have been deleted . If there is, the handler and IServiceScope
will be released.
If there are still LifetimeTrackingHttpMessageHandler
active references to any handlers, put the entry back into the queue and start the cleanup timer again.
Four, summary
If you see here, it means you are still very patient.
This article is a study of the source code, which can help us understand IHttpClientFactory
how it works, and how it fills the old HttpClient
pit.
Sometimes it's helpful to look at the source code.
If you like it, come to a three-in-one, let more people benefit from you