You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 03:11:26