如何在自定义ConsoleFormatter中获取DI的Scoped服务值?
需要在自定义ConsoleFormatter中获取DI提供的Scoped服务IIdService的值,该服务会为每个新请求生成唯一值并在请求周期内保持不变。尝试通过将IServiceProvider存入自定义ConsoleFormatterOptions的方式获取服务,但得到的RequestId为空,猜测原因是:
- 没有当前请求的
HttpContext(IIdService通过请求头获取或创建ID) - 使用的是启动时构建的旧
ServiceProvider,与当前请求的作用域无关
同时对.NET中ConsoleFormatterOptions的Options模式存在疑问,不确定是否需要通过失效选项重新执行启动逻辑。
核心问题分析
你的思路误区在于:ConsoleFormatter是单例服务,而IIdService是Scoped服务,直接从启动时的根ServiceProvider获取Scoped服务会脱离当前请求的作用域,自然拿不到请求上下文相关的RequestId。另外,Options模式主要用于配置的动态重载,不适合传递服务容器这类运行时依赖。
正确实现方式
1. 利用IExternalScopeProvider获取请求作用域
ASP.NET Core日志系统会自动将当前请求的作用域关联到日志条目,我们可以通过IExternalScopeProvider遍历作用域,拿到当前请求的服务作用域后获取Scoped服务:
public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable { static long RowIndex = 1; readonly IDisposable _optionsReloadToken; CustomConsoleFormatterOptions _formatterOptions; private readonly IServiceProvider _rootServiceProvider; // 注入根IServiceProvider(单例) public CustomLoggingFormatter( IOptionsMonitor<CustomConsoleFormatterOptions> options, IServiceProvider rootServiceProvider) : base(nameof(CustomLoggingFormatter)) { (_optionsReloadToken, _formatterOptions) = (options.OnChange(ReloadLoggerOptions), options.CurrentValue); _rootServiceProvider = rootServiceProvider; } public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter) { string message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception); if (message is null) return; string requestId = string.Empty; // 遍历作用域,提取当前请求的服务作用域 scopeProvider?.ForEachScope((scope, state) => { if (scope is IServiceScope serviceScope) { var idService = serviceScope.ServiceProvider.GetService<IIdService>(); if (idService != null) { requestId = idService.RequestId; } } }, null); // 输出带RequestId的日志 textWriter.WriteLine($"[{requestId}] {message}"); } void ReloadLoggerOptions(CustomConsoleFormatterOptions options) => _formatterOptions = options; public void Dispose() => _optionsReloadToken?.Dispose(); }
2. 简化服务注册逻辑
不需要在CustomConsoleFormatterOptions中存储IServiceProvider,直接通过构造函数注入根服务容器即可:
public static class ConsoleLoggerExtensions { public static ILoggingBuilder AddCustomConsoleFormatter( this ILoggingBuilder builder, Action<CustomConsoleFormatterOptions> options = null) { builder.AddConsole(opt => opt.FormatterName = nameof(CustomLoggingFormatter)); if (options != null) { builder.Services.Configure(options); } return builder.AddConsoleFormatter<CustomLoggingFormatter, CustomConsoleFormatterOptions>(); } }
启动配置简化为:
services.AddLogging(opt => opt.AddCustomConsoleFormatter());
3. Options模式的正确用法说明
ConsoleFormatterOptions的Options模式仅用于配置参数的动态更新(比如日志格式、颜色开关等),不需要通过失效选项来重新执行启动逻辑。你的场景中完全不需要用Options传递服务容器,直接构造函数注入根IServiceProvider即可。
补充验证点
确保IIdService的注册和实现正确,依赖IHttpContextAccessor获取请求上下文:
public interface IIdService { string RequestId { get; } } public class IdService : IIdService { private readonly IHttpContextAccessor _httpContextAccessor; public IdService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; RequestId = GetOrCreateRequestId(); } public string RequestId { get; } private string GetOrCreateRequestId() { var context = _httpContextAccessor.HttpContext; if (context == null) return Guid.NewGuid().ToString(); if (context.Request.Headers.TryGetValue("X-Request-Id", out var id)) { return id.ToString(); } var newId = Guid.NewGuid().ToString(); context.Response.Headers.Append("X-Request-Id", newId); return newId; } } // 注册服务 services.AddScoped<IIdService, IdService>(); services.AddHttpContextAccessor(); // 必须注册才能获取HttpContext
内容的提问来源于stack exchange,提问作者steb

