自定义 ConsoleFormatter 中的 DI/HttpContext

DI/HttpContext in custom ConsoleFormatter

提问人:steb 提问时间:11/7/2023 最后编辑:Stevensteb 更新时间:11/7/2023 访问量:31

问:

在我的自定义中,我需要从添加到 DI 的服务中获取值。此值保存在 SCOPED 服务中,因为它为每个新请求生成一个新值,但应通过请求保留该值。ConsoleFormatter

问题是我不知道如何在 中访问 DI 服务,我在下面尝试的是将服务容器放入 options 参数中。我的想法是,.NET 服务是一个单一实例,这样我就可以在日志记录时请求我的服务并获取我的范围服务。ConsoleFormatter

public static class ConsoleLoggerExtensions
{
    public static ILoggingBuilder AddCustomConsoleFormatter(
    this ILoggingBuilder builder, Action<CustomConsoleFormatterOptions> options)
    {
        return builder.AddConsole(
            options => options.FormatterName = nameof(CustomLoggingFormatter))
            .AddConsoleFormatter<
                CustomLoggingFormatter, CustomConsoleFormatterOptions>(
                options);
    }
}

public sealed class CustomConsoleFormatterOptions : ConsoleFormatterOptions
{
    public IServiceProvider serviceProvider { get; set; }
}

public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable
{
    static long RowIndex = 1;
    readonly IDisposable _optionsReloadToken;
    CustomConsoleFormatterOptions _formatterOptions;

    public CustomLoggingFormatter(
        IOptionsMonitor<CustomConsoleFormatterOptions> options)
        : base(nameof(SebLoggingFormatter))
    {
        (_optionsReloadToken, _formatterOptions) =
            (options.OnChange(ReloadLoggerOptions), options.CurrentValue);
    }

    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;

        var idService =
            _formatterOptions.serviceProvider.GetService<IIdService>();
        var requestId = idService.RequestId;
    }

    void ReloadLoggerOptions(CustomConsoleFormatterOptions options) =>
        _formatterOptions = options;

    public void Dispose() =>
        _optionsReloadToken?.Dispose();
}

在我的启动中,我添加了一个 Task 来获取 CustomFormnaytterOptions

services.AddLogging(opt => opt.AddCustomConsoleFormatter(options =>
{
    var serviceProvider = services.BuildServiceProvider();
    options.serviceProvider = serviceProvider;
}));

但是当我从服务请求 IdService 时,我会得到一个值为空的服务,对我来说这可能是因为:

  • 没有 http-context(IdService 正在通过当前请求中的标头选取或创建 Id)
  • 我在这里使用的 .net 服务是启动时的旧服务(=不是单例),因此与当前请求无关

我不明白的一件事是(ConsoleFormatterOptions 使用)在 .net 中的这种选项编码模式似乎是一个内置功能。我是否应该以某种方式尝试使选项失效,以便它从启动重新运行任务?

C# .NET 日志记录 控制台 依赖项反转

评论

0赞 steb 11/7/2023
所以我在这里做了一些调试(在 IdService 的构造函数中添加了一个创建时间),我从 .net Services 返回的 IdService 确实是启动时的旧 - 我希望从请求中获得当前版本。我猜 CustomConsole 没有 HttpContext

答:

1赞 Steven 11/7/2023 #1

您的解决方案存在以下几个问题,导致其无法正常工作:

  • 将该字段设置为 将导致格式化程序获取所有 DI 注册的副本。这不会授予您访问请求的权限,也不会授予您访问活动范围或 HTTP 请求的权限。CustomConsoleFormatterOptions.serviceProviderservices.BuildServiceProvider()IIdService
  • 多次调用是一种有问题的做法,这就是为什么 .NET Core 中有一个代码分析器会警告你这一点。BuildServiceProvider
  • 记录器及其格式化程序始终被框架假定为单例。尽管格式化程序可以注入依赖关系,但这些依赖关系本身已经、应该或将要成为单例。如果注入 ,它将是“根”服务提供程序,并且不会授予你访问作用域内服务的权限。IServiceProvider

解决方案是注入类,因为它允许访问当前:IHttpContextAccessorHttpContext

public sealed class CustomLoggingFormatter : ConsoleFormatter
{
    private readonly IHttpContextAccessor accessor;

    public CustomLoggingFormatter(IHttpContextAccessor accessor)
        : base(nameof(SebLoggingFormatter))
    {
        this.accessor = accessor;
    }

    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;

        // Pull scoped service from request
        var idService =
            this.accessor.HttpContext.RequestServices.GetService<IIdService>();

        var requestId = idService.RequestId;
    }
}

这可以按如下方式连接:

builder.AddHttpAccessor();

builder.AddConsole(
    options => options.FormatterName = nameof(CustomLoggingFormatter))
    .AddConsoleFormatter<
        CustomLoggingFormatter, CustomConsoleFormatterOptions>();

评论

0赞 steb 11/7/2023
太棒了,哈哈,太简单了,只需注入 IHttpContextAccessor :-)我确实设法通过做同样的事情让它工作,首先使用我的服务并请求 HttpContextAccessor,然后从该 DiContainer 请求 IdService。但当然,你的方式是唯一正确的方式。非常感谢您的帮助!