自定义 ConsoleFormatter 中的 DI/HttpContext

问题描述 投票:0回答:1

在我的自定义中

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

问题是我不知道如何访问

ConsoleFormatter
中的DI服务,我下面尝试的是将服务容器放入选项参数中。我的想法是,.NET 服务是一个单例,这样我就可以在日志记录时请求我的服务并获得我的作用域服务。

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();
}

在我的启动中,我添加一个任务来获取 CustomFormnaytterOptions

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

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

  • 没有http上下文(IdService正在通过当前请求中的标头选择或创建Id)
  • 我在这里使用的 .net 服务从启动时起就已经很旧了(=不是单例),因此与当前请求无关

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

c# .net logging console dependency-inversion
1个回答
1
投票

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

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

解决方案是注入

IHttpContextAccessor
类,因为它允许访问当前的
HttpContext
:

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>();
© www.soinside.com 2019 - 2024. All rights reserved.