如何从Singleton日志器访问.NET 6的Scoped对象?
解决方案:利用Serilog LogContext传递Scoped上下文数据
你的核心问题在于Serilog格式化器是Singleton生命周期,无法直接访问Scoped服务。解决思路是把Scoped上下文数据提前注入到日志事件的属性中,而非在格式化器里尝试获取Scoped服务。具体步骤如下:
1. 利用LogContext推送上下文数据
在处理队列任务的作用域内,将ICustomContext中的数据推送到Serilog的LogContext(基于AsyncLocal,能跟踪异步上下文):
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var workItem = await _queue.DequeueAsync(stoppingToken); // 创建当前任务的作用域 using var scope = _serviceProvider.CreateScope(); var customContext = scope.ServiceProvider.GetRequiredService<ICustomContext>(); // 从队列消息或其他来源设置上下文数据 customContext.CorrelationId = workItem.CorrelationId; customContext.TraceId = Guid.NewGuid().ToString(); // 将上下文数据推送到LogContext,自动关联当前异步上下文 using (LogContext.PushProperty("CorrelationId", customContext.CorrelationId)) using (LogContext.PushProperty("TraceId", customContext.TraceId)) { try { await workItem.ProcessAsync(scope.ServiceProvider, stoppingToken); _logger.LogInformation("任务处理完成"); } catch (Exception ex) { _logger.LogError(ex, "任务处理失败"); } } } }
2. 启用Serilog的LogContext增强
在Serilog配置中开启Enrich.FromLogContext(),让日志事件自动包含LogContext中的属性:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog((context, services, config) => config .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() // 启用LogContext属性增强 .WriteTo.Console(new CustomFormatter()) ) .ConfigureServices(services => { services.AddScoped<ICustomContext, CustomContext>(); services.AddHostedService<Worker>(); // 其他服务注册 });
3. 修改自定义格式化器,直接读取日志属性
不再依赖IServiceProvider,从LogEvent的Properties中提取上下文数据:
internal class CustomFormatter : ITextFormatter { private readonly ILogProvider _provider; static readonly JsonSerializerSettings _jsonOptions = new() { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new CamelCasePropertyNamesContractResolver() }; public CustomFormatter(ILogProvider provider) { _provider = provider; } public void Format(LogEvent logEvent, TextWriter output) { var logObj = _provider.Create(logEvent); // 从日志事件属性中提取CorrelationId if (logEvent.Properties.TryGetValue("CorrelationId", out var correlationId)) { logObj.CorrelationId = correlationId.ToString().Trim('"'); } // 同理提取TraceId等其他上下文数据 if (logEvent.Properties.TryGetValue("TraceId", out var traceId)) { logObj.TraceId = traceId.ToString().Trim('"'); } output.WriteLine(JsonConvert.SerializeObject(logObj, _jsonOptions)); } }
优化:自动推送上下文的Scoped服务
如果不想手动写PushProperty,可以封装一个Scoped服务,自动完成上下文推送和清理:
public class CustomContextScope : IDisposable { private readonly IDisposable _logScope; public CustomContextScope(ICustomContext customContext) { // 批量推送上下文属性 _logScope = LogContext.PushProperties(new Dictionary<string, object> { ["CorrelationId"] = customContext.CorrelationId, ["TraceId"] = customContext.TraceId }); } public void Dispose() { _logScope.Dispose(); } } // 注册为Scoped服务 services.AddScoped<CustomContextScope>();
使用时只需在作用域内获取该服务即可:
using var scope = _serviceProvider.CreateScope(); var customContext = scope.ServiceProvider.GetRequiredService<ICustomContext>(); customContext.CorrelationId = workItem.CorrelationId; customContext.TraceId = Guid.NewGuid().ToString(); // 自动推送上下文到LogContext using var _ = scope.ServiceProvider.GetRequiredService<CustomContextScope>(); // 处理任务 await workItem.ProcessAsync(scope.ServiceProvider, stoppingToken);
内容的提问来源于stack exchange,提问作者Kyore
相关产品推荐
相关产品推荐

