如何實現Http請求報頭的自動轉發[設計篇]

HeaderForwarder組件不僅能夠從當前接收請求提取指定的HTTP報頭,並自動將其添加到任何一個通過HttpClient發出的請求中,它同時也提供了一種基於Context/ContextScope的編程模式是我們可以很方便地將任何報頭添加到指定範圍內的所有由HttpClient發出的請求中。上篇介紹了HeaderForwarder組件的使用方式,現在我們來簡單聊聊該組件的設計和實現原理。[源程式碼從這裡下載]

目錄
一、HeaderForwardObserver
二、HttpClientObserver
三、HeaderForwaderStartupFilter
四、HttpInvocationContext/HttpInvocationContextScope
五、OutgoingHeaderCollectionProvider
六、服務註冊

一、HeaderForwardObserver

HeaderForwarder組件利用HeaderForwardObserver對HttpClient進行攔截,並將需要的報頭添加到由它發出的請求消息中,我們曾經在《四種為HttpClient添加默認請求報頭的解決方案》一文中介紹過這種方案,這也是大部分APM自動添加跟蹤報頭的解決方案。具體的原理其實很簡單:當HttpClient發送請求過程中會利用DiagnosticListener觸發一些列事件,並在事件中提供相應的對象,比如發送的HttpRequestMessage和接收的HttpResponseMessage。如果我們需要這個過程進行干預,只需要訂閱相應的事件並將干預操作實現在提供的回調中。《ASP.NET Core 3框架揭秘》第8「診斷日誌」具有對DiagnosticListener的詳細介紹。

HeaderForwarder用來添加請求報頭的是一個類型為HeaderForwardObserver的對象。在介紹該類型之前,我們得先來介紹如下這個IOutgoingHeaderCollectionProvider介面,顧名思義,它用來提供需要被添加的所有HTTP請求報頭。

public interface IOutgoingHeaderCollectionProvider
{
    IDictionary<string, StringValues> GetHeaders();
}

如下所示的是HeaderForwardObserver的定義。如程式碼片段所示,HeaderForwardObserver實現了IObserver<KeyValuePair<string, object>>
接。在實現的OnNext中,通過對事件名稱(System.Net.Http.HttpRequestOut.Start)的比較訂閱了HttpClient在發送請求前觸發的事件,並從提供的參數提取出表示待發送請求的HttpRequestMessage對象(對應Request屬性)。有了這個待發送的請求,我們只需要從構造函數中注入的IOutgoingHeaderCollectionProvider 對象提取出所有報頭列表,並將其添加這個HttpRequestMessage對象中即可。

public sealed class HeaderForwardObserver : IObserver<KeyValuePair<string, object>>
{
    private static Func<object, HttpRequestMessage> _requestAccessor;
    private readonly IOutgoingHeaderCollectionProvider _provider;
   
    public HeaderForwardObserver(IOutgoingHeaderCollectionProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
    }
   
    public void OnCompleted() { }
    public void OnError(Exception error) { }
    public void OnNext(KeyValuePair<string, object> value)
    {
        if (headers.Any() && value.Key == "System.Net.Http.HttpRequestOut.Start")
        {
             var headers = _provider.GetHeaders();
            _requestAccessor ??= CreateRequestAccessor(value.Value.GetType());
            var outgoingHeaders = _requestAccessor(value.Value).Headers;
            foreach (var kv in headers)
            {
                outgoingHeaders.Add(kv.Key, kv.Value.AsEnumerable());
            }
        }
    }

    private static Func<object, HttpRequestMessage> CreateRequestAccessor(Type type)
    {
        var requestProperty = type.GetProperty("Request");
        var payload = Expression.Parameter(typeof(object));
        var convertToPayload = Expression.Convert(payload, type);
        var getRequest = Expression.Call(convertToPayload, requestProperty.GetMethod);
        var convertToRequest = Expression.Convert(getRequest, typeof(HttpRequestMessage));
        return Expression.Lambda<Func<object, HttpRequestMessage>>(convertToRequest, payload).Compile();
    }
}

二、HttpClientObserver

HeaderForwardObserver藉助於如下這個HttpClientObserver進行註冊。如程式碼片段所示,HttpClientObserver 實現了IObserver<DiagnosticListener>介面,在實現的OnNext方法中,它創建出HeaderForwardObserver對象並將其訂閱到HttpClient使用的DiagnosticListener對象上(該對象的名稱為HttpHandlerDiagnosticListener)。

public sealed class HttpClientObserver : IObserver<DiagnosticListener>
{
    private readonly IOutgoingHeaderCollectionProvider _provider;
    public HttpClientObserver(IOutgoingHeaderCollectionProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
    }    
    public void OnCompleted() { }
    public void OnError(Exception error) { }
    public void OnNext(DiagnosticListener value)
    {
        if (value.Name == "HttpHandlerDiagnosticListener")
        {
            value.Subscribe(new HeaderForwardObserver(_provider));
        }
    }
}

三、HeaderForwaderStartupFilter

我們將針對HttpClientObserver的註冊實現在如下這個HeaderForwaderStartupFilter類型中。如程式碼片段所示,HeaderForwaderStartupFilter實現了IStartupFilter介面,針對HttpClientObserver的註冊就實現在Configure方法中。

public sealed class HeaderForwaderStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app => {
            DiagnosticListener.AllListeners.Subscribe(app.ApplicationServices.GetRequiredService<HttpClientObserver>());
            next(app);
        };
    }
}

四、HttpInvocationContext/HttpInvocationContextScope

接下來我們討論待轉發HTTP報頭的來源問題。通過上篇的介紹我們知道,帶轉發報頭有兩種來源,一種是從當前請求中提取出來的,另一種是手工添加到HttpInvocationContext上下文中。如下所示的是HttpInvocationContext的定義,我們添加的報頭就存儲在它的OutgoingHeaders 屬性中,表示當前上下文的HttpInvocationContext對象存儲在AsyncLocal<HttpInvocationContext>對象上。

public sealed class HttpInvocationContext
{
    internal static readonly AsyncLocal<HttpInvocationContext> _current = new AsyncLocal<HttpInvocationContext>();
    public static HttpInvocationContext Current => _current.Value;
    public IDictionary<string, StringValues> OutgoingHeaders { get; } = new Dictionary<string, StringValues>();
    internal HttpInvocationContext() { }
}

HttpInvocationContextScope用來控制HttpInvocationContext的範圍(生命周期),從定義可以看出,只有在創建該Scope的using block範圍為才能得到當前的HttpInvocationContext上下文。

public sealed class HttpInvocationContextScope : IDisposable
{
    public HttpInvocationContextScope()
    {
        HttpInvocationContext._current.Value = new HttpInvocationContext();
    }
    public void Dispose() => HttpInvocationContext._current.Value = null;
}

五、OutgoingHeaderCollectionProvider

HeaderForwardObserver添加到請求消息中的報頭是通過注入的IOutgoingHeaderCollectionProvider對象提供的,現在我們來看看該介面的實現類型OutgoingHeaderCollectionProvider。我們說過,所有的報頭具有兩個來源,其中一個來源於當前接收的請求,但是並不是請求中攜帶的所有報頭都需要轉發,所以我們需要利用如下這個HeaderForwarderOptions類型來配置轉發的報頭名稱。

public class HeaderForwarderOptions
    public ISet<string> AutoForwardHeaderNames { get; } = new HashSet<string>();
    public void AddHeaderNames(params string[] headerNames) => Array.ForEach(headerNames, it => AutoForwardHeaderNames.Add(it));
}

如下所示的是OutgoingHeaderCollectionProvider類型的定義。在實現的GetHeaders方法中,它利用注入的IHttpContextAccessor 對象得到當前HttpContext,並結合HeaderForwarderOptions上的配置得到需要自動轉發的報頭。然後通過當前HttpInvocationContext上下文你得到手工指定的報頭,兩者合併之後成為了最終需要添加到請求消息的報頭列表。

public sealed class OutgoingHeaderCollectionProvider : IOutgoingHeaderCollectionProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISet<string> _autoForwardedHeaderNames;

    public OutgoingHeaderCollectionProvider(IHttpContextAccessor httpContextAccessor, IOptions<HeaderForwarderOptions> optionsAccessor)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _autoForwardedHeaderNames = (optionsAccessor?? throw new ArgumentNullException(nameof(optionsAccessor))).Value.AutoForwardHeaderNames;
    }

    public IDictionary<string, StringValues> GetHeaders()
    {
        var headers = new Dictionary<string, StringValues>();
        try
        {
            var incomingHeaders = _httpContextAccessor.HttpContext?.Request?.Headers;
            if (incomingHeaders != null)
            {
                foreach (var headerName in _autoForwardedHeaderNames)
                {
                    if (incomingHeaders.TryGetValue(headerName, out var values))
                    {
                        headers.Add(headerName, values);
                    }
                }
            }
        }
        catch (ObjectDisposedException) {}

        var outgoingHeaders = HttpInvocationContext.Current?.OutgoingHeaders;
        if (outgoingHeaders != null)
        {
            foreach (var kv in outgoingHeaders)
            {
                if (headers.TryGetValue(kv.Key, out var values))
                {
                    headers[kv.Key] = new StringValues(values.Concat(kv.Value).ToArray());
                }
                else
                {
                    headers.Add(kv.Key, kv.Value);
                }
            }
        }

        return headers;
    }
}

到目前為止,HeaderForwarder的核心成員均已介紹完畢,這些介面/類型之間的關係體現在如下所示的UML中。

image

六、服務註冊

HeaderForwarder涉及的服務通過如下這個AddHeaderForwarder擴展方法進行註冊

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddHeaderForwarder(this IServiceCollection services, Action<HeaderForwarderOptions> setup = null)
    {
        services = services ?? throw new ArgumentNullException(nameof(services));
        services.AddOptions();
        services.AddHttpContextAccessor();
        services.TryAddSingleton<IOutgoingHeaderCollectionProvider, OutgoingHeaderCollectionProvider>();
        services.TryAddSingleton<HttpClientObserver>();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, HeaderForwaderStartupFilter>());
        if (null != setup)
        {
            services.Configure(setup);
        }
        return services;
    }
}

我們進一步定義了針對IHostBuilder介面的擴展方法,我們在前面演示實例中正是使用的這個方法。

public static class HostBuilderExtensions
{
    public static IHostBuilder UseHeaderForwarder(this IHostBuilder hostBuilder, Action<HeaderForwarderOptions> setup = null)
    {
        hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
        hostBuilder.ConfigureServices((_,services) => services.AddHeaderForwarder(setup));
        return hostBuilder;
    }
}

如何實現Http請求報頭的自動轉發[應用篇]
如何實現Http請求報頭的自動轉發[設計篇]