. NET Core HttpClientFactory+Consul for service discovery

preface

Last article . NET Core HttpClient+Consul to realize service discovery It has been mentioned that HttpClient has the problem of socket delay release, and the problem of server denial of service caused by port number exhaustion caused by high concurrency. Fortunately, Microsoft realized this problem and started to introduce HttpClientFactory in. Net core version 2.1 to make up for this problem. For more detailed introduction of HttpClientFactory, please refer to the official documents of Microsoft https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#httpclient-and-lifetime-management We learned that to inject the custom HttpMessageHandler into the HttpClient, we must use the constructor. Next, we will gradually find out how to use our custom Handler for HttpClientFactory.

Creation of HttpClient

I'm sure you all know how to use HttpClientFactory services.AddHttpClient() start by injecting related classes. Paste the source address first HttpClientFactoryServiceCollectionExtensions source code Then let's take a look at the implementation methods we are concerned about, which are roughly as follows: the code has been deleted

/// <summary>
/// Adds the <see cref="IHttpClientFactory"/> and related services to the <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddHttpClient(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }

    .....

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

    .....

    return services;
}

Through the source code, we can see that the implementation class injection of IHttpClientFactory is actually DefaultHttpClientFactory. Let's continue to search according to the source code DefaultHttpClientFactory source address We found a familiar name 😄😄😄

public HttpClient CreateClient(string name)
{
    if (name == null)
    {
        throw new ArgumentNullException(nameof(name));
    }
    var handler = CreateHandler(name);
    var client = new HttpClient(handler, disposeHandler: false);
    var options = _optionsMonitor.Get(name);
    for (var i = 0; i < options.HttpClientActions.Count; i++)
    {
        options.HttpClientActions[i](client);
    }
    return client;
}

Here we find that the CreateHandler method creates the handler and passes in HttpClient. Continue to look down and find this code

public HttpMessageHandler CreateHandler(string name)
{
    if (name == null)
    {
        throw new ArgumentNullException(nameof(name));
    }
    var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
    StartHandlerEntryTimer(entry);
    return entry.Handler;
}

And then we_ The entryFactory delegation, then keep looking for it

internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
    .....
    try
    {
        var builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
        builder.Name = name;
        Action<HttpMessageHandlerBuilder> configure = Configure;
        for (var 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);

        .....
    }
    catch
    {
        .....
    }
}

Found HttpMessageHandlerBuilder, built HttpMessageHandler from it, eh! As if I've seen it somewhere, I suddenly realized it was in AddHttpClient extension method

services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();

And then we found it DefaultHttpMessageHandlerBuilder Here I see familiar figures
There's a rush of emotion in finding here, that is to say, just replace the default httpclientandler I implemented, but I feel like there's no place to start. At this time, I suddenly thought that DefaultHttpMessageHandlerBuilder is registered. Then I can implement a ConsulHttpMessageHandlerBuilder to replace the DefaultHttpMessageHandlerBuilder registered by default. It will take a while. I have written the following implementation

Custom HttpMessageHandlerBuilder

public class ConsulHttpMessageHandlerBuilder: HttpMessageHandlerBuilder
{
    public ConsulHttpMessageHandlerBuilder(ConsulDiscoveryHttpClientHandler consulDiscoveryHttpClientHandler)
    {
        PrimaryHandler = consulDiscoveryHttpClientHandler;
    }
    private string _name;
    public override IList<DelegatingHandler> AdditionalHandlers => new List<DelegatingHandler>();
    public override string Name {
        get => _name;
        set
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }
            _name = value;
        }
    }
    public override HttpMessageHandler PrimaryHandler { get; set; }
    public override HttpMessageHandler Build()
    {
        if (PrimaryHandler == null)
        {
            throw new InvalidOperationException(nameof(PrimaryHandler));
        }
        return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
    }
}

Compared with the original code, it has actually changed a little, that is to replace the default httpclientandler with its own ConsulDiscoveryHttpClientHandler. For the implementation of ConsulDiscoveryHttpClientHandler, please refer to the implementation of the previous article. Then replace the default httpmessagehandlerbuilder in the registered place.

public void ConfigureServices(IServiceCollection services)
{
      services.AddHttpClient();
      services.AddTransient<ConsulDiscoveryHttpClientHandler>();
      services.Replace(new ServiceDescriptor(typeof(HttpMessageHandlerBuilder),typeof(ConsulHttpMessageHandlerBuilder),ServiceLifetime.Transient));
}

Tried, no problem, secretly happy for a few seconds. But calm down and think about it. It's not very reasonable to write Builder to replace the default way. It does not conform to the principle of open and closed, and the invasion of the original code itself is relatively large, which seems not very reasonable. Otherwise, we must study carefully, especially at the beginning, we can step on many pits less. At Microsoft Help documents The extension method of IHttpClientBuilder can replace the default PrimaryHandler instance with a user-defined implementation, which is mentioned in. The roughly modified registration places are as follows.

public void ConfigureServices(IServiceCollection services)
{
      services.AddTransient<ConsulDiscoveryHttpClientHandler>();
      services.AddHttpClient().ConfigurePrimaryHttpMessageHandler<ConsulDiscoveryHttpClientHandler>();;
}

HttpClientBuilderExtensions extension class implementation

Next, let's see what the extended method ConfigurePrimaryHttpMessageHandler does. The method comes from HttpClientBuilderExtensions extension class The specific implementation is as follows

public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(this IHttpClientBuilder builder)
            where THandler : HttpMessageHandler
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }
    builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
    {
        options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = b.Services.GetRequiredService<THandler>());
    });
    return builder;
}

And then through CreateHandlerEntry method of DefaultHttpClientFactory class You can see it in HttpMessageHandlerBuilderActions of HttpClientFactoryOptions class In fact, the call place passed in is the DefaultHttpMessageHandlerBuilder registered in HttpMessageHandlerBuilder. The general call code is as follows

internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
    .....
    try
    {
        var builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
        builder.Name = name;
        Action<HttpMessageHandlerBuilder> configure = Configure;
        for (var 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 (var i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
            {
                options.HttpMessageHandlerBuilderActions[i](b);
            }
        }
    }
    catch
    {
        .....
    }
}

Looking back, there is another HttpClientBuilderExtensions extension class ConfigureHttpMessageHandlerBuilder extension method

public static IHttpClientBuilder AddHttpMessageHandler<THandler>(this IHttpClientBuilder builder)
            where THandler : DelegatingHandler
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }
    builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
    {
        options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService<THandler>()));
    });
    return builder;
}

This is to add a Handler attached to DefaultHttpMessageHandlerBuilder. What is the relationship between PrimaryHandler and AdditionalHandlers? Let's look back DefaultHttpMessageHandlerBuilder class related methods The general code is as follows

public override HttpMessageHandler Build()
{
    if (PrimaryHandler == null)
    {
        var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler));
        throw new InvalidOperationException(message);
    }    
    return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
}

protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable<DelegatingHandler> additionalHandlers)
{
    if (primaryHandler == null)
    {
        throw new ArgumentNullException(nameof(primaryHandler));
    }
    if (additionalHandlers == null)
    {
        throw new ArgumentNullException(nameof(additionalHandlers));
    }

    var additionalHandlersList = additionalHandlers as IReadOnlyList<DelegatingHandler> ?? additionalHandlers.ToArray();
    var next = primaryHandler;
    for (var i = additionalHandlersList.Count - 1; i >= 0; i--)
    {
        var handler = additionalHandlersList[i];
        if (handler == null)
        {
            var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers));
            throw new InvalidOperationException(message);
        }
        if (handler.InnerHandler != null)
        {
            var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid(
                nameof(DelegatingHandler.InnerHandler),
                nameof(DelegatingHandler),
                nameof(HttpMessageHandlerBuilder),
                Environment.NewLine,
                handler);
            throw new InvalidOperationException(message);
        }
        handler.InnerHandler = next;
        next = handler;
    }
    return next;
}

It can be seen from this code that a Handler execution pipeline was built with PrimaryHandler and AdditionalHandlers collection. PrimaryHandler is the last execution point of the pipeline, and the additional pipeline is executed in the order of code injection. I believe you have a certain understanding of the general working mode of HttpClientFactory. In fact, from the coding point of view, unless there are special requirements, we will not replace the PrimaryHandler, just add our Handler to the AdditionalHandlers collection.

Final implementation mode

Through the above analysis, we can basically implement the most reasonable way by hand

public void ConfigureServices(IServiceCollection services)
{
    //Consumer address
    services.AddConsul("http://localhost:8500/");
    //Httpclient lookup name (service registration name is recommended)
    services.AddHttpClient("PersonService", c =>
    {
        //The name of the service registration (it is recommended to be the same as the httpclient lookup name)
        c.BaseAddress = new Uri("http://PersonService/");
    }).AddHttpMessageHandler<ConsulDiscoveryDelegatingHandler>();
}

AddConsul extension method

public static IServiceCollection AddConsul(this IServiceCollection services, string consulAddress)
{
    services.AddTransient(provider => {
        return new ConsulClient(x =>
        {
            // Consumer service address
            x.Address = new Uri(consulAddress);
        });
    });
    //Register a custom DelegatingHandler
    services.AddTransient<ConsulDiscoveryDelegatingHandler>();
    return services;
}

Custom ConsulDiscoveryDelegatingHandler

public class ConsulDiscoveryDelegatingHandler : DelegatingHandler
{
     private readonly ConsulClient _consulClient;
     private readonly ILogger<ConsulDiscoveryDelegatingHandler> _logger;
     public ConsulDiscoveryDelegatingHandler(ConsulClient consulClient, 
           ILogger<ConsulDiscoveryDelegatingHandler> logger)
     {
        _consulClient = consulClient; 
        _logger = logger;
     }
            
     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
          var current = request.RequestUri;
          try
          {              
              //The domain name (host name) in the called service address can be transferred to the discovered service name
              request.RequestUri = new Uri($"{current.Scheme}://{LookupService(current.Host)}/{current.PathAndQuery}");
              return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
          }
          catch (Exception e)
          {
              _logger?.LogDebug(e, "Exception during SendAsync()");
              throw;
          }
          finally
          {
              request.RequestUri = current;
          }
      }

      private string LookupService(string serviceName)
      {
          var services = _consulClient.Catalog.Service(serviceName).Result.Response;
          if (services != null && services.Any())
          {
                //Analog load balancing algorithm (randomly obtaining an address)
                int index = r.Next(services.Count());
                var service = services.ElementAt(index);
                return $"{service.ServiceAddress}:{service.ServicePort}");
           }
           return null;
      }
}

Write personal test controller test code

public class PersonTestController : Controller
{
    private readonly IHttpClientFactory _clientFactory;
    public PersonTestController(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<ActionResult<string>> GetPersonInfo(int personId)
    {
        var client = _clientFactory.CreateClient("PersonService");
        var response = await client.GetAsync($"/Person/Get/{personId}");
        var result = await response.Content.ReadAsStringAsync();
        return result;
    }
}

summary

Through these two articles, it mainly explains how HttpClientFactory and HttpClient combine with Consul to complete service discovery. It is more recommended for individuals to use HttpClientFactory in subsequent development and practice. This paper may focus on thinking, the specific implementation may not be refined enough. It also involves part of the framework source code. If you are not familiar with the source code, you may not have a good understanding of some places. In addition, you may not have enough writing. If it is inconvenient to read, please understand. I still want to convey my understanding and thoughts to you, and hope to criticize and guide you so as to correct later.

Tags: socket less

Posted on Tue, 26 May 2020 01:03:44 -0400 by Johannes80