繁体   English   中英

HttpClient 避免在异步请求期间进行处理

[英]HttpClient avoid disposing during async request

我正在使用HttpClient.SendAsync()在 .NET Framework (C#) 中发送安静的服务请求。

在某些情况下,我想记录请求和响应,例如,当响应代码是特定的 HTTP 状态代码时,我想记录请求和响应。

请求(当然)是HttpRequestMessage类型,响应是HttpResponseMessage类型。

这是我现在正在使用的代码:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    HttpResponseMessage response = null;
    Exception exception = null;
    try
    {
        response = await InstanceClient.SendAsync(request ?? throw new ArgumentNullException(nameof(request)), completionOption, cancellationToken);
    }
    catch (Exception e)
    {
        exception = e;
        throw;
    }
    finally
    {
        await LogDelegateInvoker(logContext, request, response, exception, cancellationToken);
    }
        
    return response;
}

问题是,有时当我的LogDelegateInvoker被调用时, HttpClient.SendAsync()代码已经处理了请求或请求内容,因此如果我的请求尝试检查(并记录)该内容,它就不能因为内容已经被释放。

我很想告诉SendAsync()方法不要处理请求,我将负责自己处理它。

有没有办法做到这一点,或其他选择?

如果您使用的是 .NET Core 3.0 以上的 .NET 版本,您应该能够避免看到已处理的请求。 (显然旧版本中存在 HttpClient 自动处理请求的错误。)进行了一些简单的更改。 使用当前代码,我看到两个可能存在问题的问题:

  1. 可以对空请求调用LogDelegateInvoker ,因为如果请求为空,您将抛出 ArgumentNullException 并立即捕获它。
  2. 您将在 catch 块中重新抛出异常,这意味着LogDelegateInvoker在调用者catch块执行后被执行。(查看此以获取更多信息))这将使调用代码能够在调用finally块之前处理请求。 根据调用代码,它还可能导致finally块永远不会被执行。

为了避免这两种情况,您可以:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request,
    HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    var httpClient = new HttpClient();
    if (request == null)
    {
        throw new ArgumentNullException(nameof(request));
    }

    HttpResponseMessage response = null;
    Exception exception = null;
    try
    {
        response = await httpClient.SendAsync(request, completionOption, cancellationToken);
    }
    catch (Exception e)
    {
        exception = e;
    }
    finally
    {
        await LogDelegateInvoker(logContext, request, response, exception, cancellationToken);
    }

    if (exception != null)
    {
        throw exception;
    }

    return response;
}

这应该防止LogDelegateInvoker在处理请求上被调用。

我找到了一个我可以接受的答案,并且可能对其他人有用。

我已经用这个替换了我的问题中的代码:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    LoggingForHttpHandler.AssociateInvokerWithRequest(request ?? throw new ArgumentNullException(nameof(request)), 
        async (req, res, ex, ct) => await LogDelegateInvoker(logContext, req, res, ex, ct));
    return await HttpClient.SendAsync(request, completionOption, cancellationToken);
}

然后在同一个类中,我有额外的私有子类和一些静态处理:

...

private static HttpClient HttpClient => _httpClient ?? (_httpClient = new HttpClient(new LoggingForHttpHandler(new HttpClientHandler())));
private static HttpClient _httpClient;

private class LoggingForHttpHandler : DelegatingHandler
{
    public LoggingForHttpHandler(HttpMessageHandler innerHandler) : base(innerHandler)
    {
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task> logDelegateInvoker = null;

        try
        {
            if (request.Headers.TryGetValues(SpecialHeaderName, out IEnumerable<string> specialHeaderValues))
            {
                request.Headers.Remove(SpecialHeaderName);
                if (ulong.TryParse(specialHeaderValues.FirstOrDefault(), out ulong logDelegateInvokerKey))
                {
                    if (!LogDelegateInvokers.TryRemove(logDelegateInvokerKey, out logDelegateInvoker))
                    {
                        logDelegateInvoker = null;
                    }
                }
            }

            response = await base.SendAsync(request, cancellationToken);
        }
        catch(Exception ex)
        {
            if (logDelegateInvoker != null)
            {
                await logDelegateInvoker(request, response, ex, cancellationToken);
            }
            throw;
        }

        if (logDelegateInvoker != null)
        {
            await logDelegateInvoker(request, response, null, cancellationToken);
        }

        return response;
    }

    public static void AssociateInvokerWithRequest (HttpRequestMessage request, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task> logDelegateInvoker)
    {
        if (logDelegateInvoker != null && request != null)
        {
            ulong logDelegateInvokerKey = (ulong)(Interlocked.Increment(ref _incrementer) - long.MinValue);
            if (LogDelegateInvokers.TryAdd(logDelegateInvokerKey, logDelegateInvoker))
            {
                request.Headers.Add(SpecialHeaderName, logDelegateInvokerKey.ToString());
            }

        }
    }

    private const string SpecialHeaderName = "__LogDelegateIndex";

    private static long _incrementer = long.MinValue;

    private static readonly ConcurrentDictionary<ulong, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task>> LogDelegateInvokers = 
        new ConcurrentDictionary<ulong, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task>>();
}

...

这里的关键点是使用自定义委托处理程序创建客户端并对其进行设置,以便委托处理程序在处理请求之前实际调用委托进行日志记录。 为支持这一点所做的是拥有一个由 64 位键索引的静态线程安全调用字典,该字典在请求提交之前呈现为字符串作为“特殊”标头,然后在它被之前从标头中剥离发送。 请注意,对HttpRequestMessage类型的请求的标头访问是同步的。

我承认这没有完美的代码味道,但它高效且快速。

我仍然很高兴能得到更好的解决方案!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM