【问题标题】:Multiple contexts in a Singleton service单例服务中的多个上下文
【发布时间】:2021-05-22 06:35:26
【问题描述】:

目前我正在设计一个记录器服务来记录由HttpClient 制作的HttpRequests

这个记录器服务是Singleton,我想在里面有scoped 上下文。

这是我的记录器:


public Logger
{
    public LogContext Context { get; set; }    

    public void LogRequest(HttpRequestLog log)
    {  
        context.AddLog(log);
    }
}

我在DelegatingHandler 中使用记录器:


private readonly Logger logger;

public LoggingDelegatingHandler(Logger logger)
{
  this.logger = logger;
}

protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
   await base.SendAsync(request, cancellationToken);
   this.logger.LogRequest(new HttpRequestog());
}

然后,当我使用HttpClient 发出一些请求时,我希望获得此特定呼叫的日志:

private void InvokeApi()
{
  var logContext = new LogContext();
  this.Logger.LogContext = logContext;
  var httpClient = httpClientFactory.CreateClient(CustomClientName);
  await httpClient.GetAsync($"http://localhost:11111");

  var httpRequestLogs = logContext.Logs;
}

问题是,它可以工作,但它不是线程安全的。如果我有InvokeApi 的并行执行,context 将不正确。

如何正确地为每次执行附加上下文?

我正在像这样注册 HttpClient:

services.AddSingleton<Logger>();
services.AddHttpClient(CentaurusHttpClient)
        .ConfigurePrimaryHttpMessageHandler((c) => new HttpClientHandler()
        {
             AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
        })
        .AddHttpMessageHandler(sp => new LoggingDelegatingHandler(sp.GetRequiredService<Logger>()));

我正在使用这个测试这段代码:

public void Test_Parallel_Logging()
{
    Random random = new Random();

    Action test = async () =>
    {
        await RunScopedAsync(async scope =>
        {
            IServiceProvider sp = scope.ServiceProvider;

            var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
            using (var context = new HttpRequestContext(sp.GetRequiredService<Logger>()))
            {
                try
                {
                    var httpClient = httpClientFactory.CreateClient();
                    await httpClient.GetAsync($"http://localhost:{random.Next(11111, 55555)}");
                }
                catch (HttpRequestException ex)
                {
                    Output.WriteLine("count: " + context?.Logs?.Count);
                }
            }
        });
    };

    Parallel.Invoke(new Action[] { test, test, test });
}

【问题讨论】:

  • 这是什么类型的应用程序? web、windows 窗体、控制台等。听起来您几乎需要在单例记录器周围使用瞬态或请求范围的包装器,并且上下文将在那里。如果是非 Web 应用程序,则可以使用某种形式的线程本地存储。然后包装器可以在日志调用期间将上下文传递给实际的记录器。
  • 您正在调用异步方法,因此需要等待它的调用,同时使您的方法异步。阅读有关异步的更多信息:docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
  • 这是一个网络项目。但将使用Hangfire 服务器运行后台服务。
  • 作为快速修复,我会尝试长期使用AsyncLocal - 我会考虑重构 Logger 并根据请求或其他方法注入它(可能使用现有的日志库之一)
  • @Zokka 是的,您创建的示例是正确的,我忘记了这个。我编辑了问题并添加了我的单元测试。

标签: c# dependency-injection


【解决方案1】:

这个记录器服务是单例的,我想在里面有作用域的上下文。

Logger 具有单例生命周期,因此其 LogContext 属性也具有单例生命周期。

对于您想要的那种范围数据,AsyncLocal&lt;T&gt; 是一个合适的解决方案。我倾向于遵循类似于 React 上下文的“提供者”/“消费者”模式,尽管“消费者”在 .NET 世界中更常见的名称是“访问者”。在这种情况下,您可以将 Logger 本身设置为提供程序,但我通常会尝试将其保留为单独的类型。

IMO 这通过提供您自己的显式范围最干净地完成,并且绑定到您的 DI 容器的“范围”生命周期。可以使用 DI 容器范围来做到这一点,但你最终会得到一些奇怪的代码,比如解析生产者,然后对它们什么都不做 - 如果未来的维护者删除(看起来是)未使用的注入类型,那么访问器就会中断。

所以我推荐你自己的范围,例如:

public Logger
{
  private readonly AsyncLocal<LogContext> _context = new();

  public void LogRequest(HttpRequestLog log)
  {
    var context = _context.Value;
    if (context == null)
      throw new InvalidOperationException("LogRequest was called without anyone calling SetContext");
    context.AddLog(log);
  }

  public IDisposable SetContext(LogContext context)
  {
    var oldValue = _context.Value;
    _context.Value = context;
    // I use Nito.Disposables; replace with whatever Action-Disposable type you have.
    return new Disposable(() => _context.Value = oldValue);
  }
}

用法(注意using 提供的明确范围):

private void InvokeApi()
{
  var logContext = new LogContext();
  using (this.Logger.SetContext(logContext))
  {
    var httpClient = httpClientFactory.CreateClient(CustomClientName);
    await httpClient.GetAsync($"http://localhost:11111");
    var httpRequestLogs = logContext.Logs;
  }
}

【讨论】:

  • 这样看起来好多了。我只是在想,如果另一个人想要这个 HttpClient 并且不向记录器提供上下文,则会在执行时引发异常。在一个完美的世界里,如果它产生一个语法错误会更好,你不这么认为吗?
  • @BrunoWarmling:如果您的意思是编译时错误,那么是的,那将是理想的。但这通常是更多的工作。例如,您可以创建HttpClient 的派生类型,它通过自己对SetContext 的调用来覆盖所有虚拟“执行请求”方法。这将强制执行正确的生产者使用,但代价是一些复杂性。您必须修改 HttpRequestLog 以强制消费者正确使用。您最终可能会子类型化整个 HttpClient 管道和工厂系统。
猜你喜欢
  • 2023-03-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-03
  • 1970-01-01
  • 1970-01-01
  • 2012-02-15
相关资源
最近更新 更多