.net 自定义ConsoleFormatter中的DI/HttpContext

hjzp0vay  于 2023-11-20  发布在  .NET
关注(0)|答案(1)|浏览(134)

在我的自定义ConsoleFormatter中,我需要从添加到DI的服务中获取一个值。该值保存在SCOPED服务中,因为它为每个新请求生成一个新值,但应该在整个请求过程中保留该值。
问题是,我不知道如何在ConsoleFormatter中访问DI服务,我在下面尝试的是将services容器放入选项参数中。我的想法是.NET Services是一个单例,这样我就可以在日志记录时请求我的服务并获得我的范围服务。

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-context(IdService正在根据当前请求中的标头拾取或创建Id)
  • 我在这里使用的.net服务是从启动时就存在的,因此与当前请求无关

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

dsf9zpds

dsf9zpds1#

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

  • CustomConsoleFormatterOptions.serviceProvider字段设置为services.BuildServiceProvider()将使格式化程序获取所有DI注册的副本。这不会给予您对请求IIdService的访问权,也不会使您访问活动范围或HTTP请求。
  • 多次调用BuildServiceProvider是一个problematic practice,这就是为什么.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>();

相关问题