单例服务中的多个上下文
Multiple contexts in a Singleton service
目前我正在设计一个记录器服务来记录 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
将不正确。
How can i have a attached context for each execution properly?
我正在这样注册 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 });
}
This logger service is Singleton and i want to have scoped contexts inside it.
Logger
有一个单例生命周期,所以它的LogContext
属性也有一个单例生命周期。
对于您想要的那种范围内的数据,AsyncLocal<T>
是一个合适的解决方案。我倾向于遵循类似于 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
制作的 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
将不正确。
How can i have a attached context for each execution properly?
我正在这样注册 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 });
}
This logger service is Singleton and i want to have scoped contexts inside it.
Logger
有一个单例生命周期,所以它的LogContext
属性也有一个单例生命周期。
对于您想要的那种范围内的数据,AsyncLocal<T>
是一个合适的解决方案。我倾向于遵循类似于 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;
}
}