使用依赖于记录器的选项验证器创建依赖于选项的自定义记录器时出现循环依赖异常

Ola*_*son 11 c# .net-core

我有一个 .NET Core 工作应用程序,想添加一个自定义文件记录器,因为Microsoft.Extensions.Logging没有提供这个。我不想为此使用额外的包(例如 Serilog)。

我将有关日志目录和日志文件的信息放入我的选项类中。这个选项类还有一个实现IValidateOptions接口的验证器。如果发生错误,此验证器会注入一个记录器实例以记录验证错误。

文件记录器提供程序需要注入选项监视器才能访问目录和文件配置。

运行应用程序时,我不幸收到异常

System.AggregateException: '某些服务无法构建'

与内容

验证服务描述符“ServiceType: Microsoft.Extensions.Hosting.IHostApplicationLifetime Lifetime: Singleton ImplementationType: Microsoft.Extensions.Hosting.Internal.ApplicationLifetime”时出错:检测到“Microsoft.Extensions.Logging.ILoggerFactory”类型的服务存在循环依赖'。Microsoft.Extensions.Hosting.IHostApplicationLifetime(Microsoft.Extensions.Hosting.Internal.ApplicationLifetime) -> Microsoft.Extensions.Logging.ILogger<Microsoft.Extensions.Hosting.Internal.ApplicationLifetime>(Microsoft.Extensions.Logging.Logger<Microsoft.Extensions .Hosting.Internal.ApplicationLifetime>) -> Microsoft.Extensions.Logging.ILoggerFactory(Microsoft.Extensions.Logging.LoggerFactory) -> System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.ILoggerProvider> ->

这是有道理的,因为当注入一个新的记录器实例时,这个实例被注入了选项。这些将触发注入新记录器实例的验证器。所以它又开始了。

记录器 => 选项 => 验证器 => 记录器 => 选项 => 验证器 => 记录器 => 选项 => 验证器 => ...

我不知道如何解决这个问题,因为我的文件记录器需要访问配置选项,而我的选项验证器应该记录验证错误。

有任何想法吗?


如果您想获得有关该应用程序的概述,这就是我为重现它所做的:

  • 创建一个新的 .NET Core Worker 项目
  • 转到 .csproj 文件并添加<FrameworkReference Include="Microsoft.AspNetCore.App" />到第一个项目组以访问 Kestrel 和 Web 内容
  • MyLib在项目中添加一个库并在主项目中引用它
  • 在库中,我创建了一个选项类,其中包含文件记录器的信息

.

public class MyOptions
{
    public string DirectoryPath { get; set; }
    public string FileName { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
  • 我还添加了一个简单的选项验证器示例,它使用了一个记录器实例

.

public class MyOptionsValidator : IValidateOptions<MyOptions>
{
    private readonly ILogger<IValidateOptions<MyOptions>> logger;

    public MyOptionsValidator(ILogger<IValidateOptions<MyOptions>> logger)
    {
        this.logger = logger;
    }

    public ValidateOptionsResult Validate(string name, MyOptions myOptions)
    {
        if (string.IsNullOrEmpty(myOptions.DirectoryPath) || string.IsNullOrEmpty(myOptions.FileName))
        {
            logger.LogWarning("Invalid");

            return ValidateOptionsResult.Fail("Invalid");
        }

        return ValidateOptionsResult.Success;
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 我将选项添加到 appsettings.json 文件中

.

"MyOptions": {
  "DirectoryPath": "C:\\Logs",
  "FileName": "log.log"
}
Run Code Online (Sandbox Code Playgroud)
  • 在库中,我扩展了服务集合以注册选项验证

.

public static class IServiceCollectionExtensions
{
    public static IServiceCollection AddMyLib(this IServiceCollection services) =>
        services.AddSingleton<IValidateOptions<MyOptions>, MyOptionsValidator>();
}
Run Code Online (Sandbox Code Playgroud)
  • 在主项目中,我设置了日志记录部分。首先我创建一个新的文件记录器。由于这不能是“正常”服务并且不会被添加到 DI 容器中,我只是期望构造函数中需要的选项

.

internal class FileLogger : ILogger
{
    private readonly string fullLogFilePath;

    public FileLogger(string logDirectoryPath, string logFileName)
    {
        if (!Directory.Exists(logDirectoryPath))
            Directory.CreateDirectory(logDirectoryPath);

        fullLogFilePath = Path.Combine(logDirectoryPath, logFileName);
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;

        using StreamWriter streamWriter = new StreamWriter(fullLogFilePath, true);
        streamWriter.WriteLine($"[{DateTime.Now}] [{logLevel}] {formatter(state, exception)} {exception?.StackTrace}");
    }

    public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

    public IDisposable BeginScope<TState>(TState state) => null;
}
Run Code Online (Sandbox Code Playgroud)
  • 为了服务这个记录器,我添加了一个提供者

.

internal class FileLoggerProvider : ILoggerProvider
{
    private readonly IOptionsMonitor<MyOptions> myOptionsMonitor;

    public FileLoggerProvider(IOptionsMonitor<MyOptions> myOptionsMonitor)
    {
        this.myOptionsMonitor = myOptionsMonitor;
    }

    public void Dispose()
    {
    }

    public ILogger CreateLogger(string categoryName)
    {
        MyOptions myOptions = myOptionsMonitor.CurrentValue;

        return new FileLogger(myOptions.DirectoryPath, myOptions.FileName);
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 并扩展日志构建器以添加文件记录器

.

public static class ILoggingBuilderExtensions
{
    public static void AddFileLogger(this ILoggingBuilder loggingBuilder)
    {
        loggingBuilder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 正如您在 Web API 项目中所做的那样,我添加了一个启动文件来配置所有选项并设置服务

.

internal class Startup
{
    private readonly IConfiguration configuration;

    public Startup(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMyLib();

        IConfigurationSection myOptionsSection = configuration.GetSection("MyOptions");
        services.Configure<MyOptions>(myOptionsSection);
    }

    public void Configure(IApplicationBuilder applicationBuilder)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 对于最后一部分,我将程序文件更新为此

.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureLogging(loggingBuilder =>
            {
                loggingBuilder
                    .ClearProviders()
                    .AddConsole()
                    .AddEventLog()
                    .AddFileLogger();
            })
            .ConfigureWebHostDefaults(webHostBuilder =>
            {
                webHostBuilder.UseKestrel().UseStartup<Startup>();
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}
Run Code Online (Sandbox Code Playgroud)
  • 由于循环依赖,您应该得到 System.AggregateException

Dan*_*dré 1

我按照@Olaf Svenson 的方法复制了这个问题,我已经能够复制了。然后我尝试了 @Martin 的方法,意识到MyOptionsValidator它需要 aILoggerFactory但它仍然导致循环依赖场景(以某种方式)。

我认为应该有一种方法来记录未处理的异常,这样您就不需要在您的日志中记录任何内容,MyOptionsValidator而是让它返回失败结果,这将导致抛出异常并被记录。但对于工人服务来说,这似乎是一个问题?假设我们无法做到这一点,然后看看我在下面提供的解决方案......

(更新:实际上您甚至不需要在下面执行此操作,但这仍然是一个很酷的挑战。不要登录您的验证器。这将防止不必要的复杂性。正常的未处理异常日志记录过程将启动并实际登录到当您的记录器配置无效时,其他记录器。超级简单且非常有效。现在您可以让各种记录器为您处理这个问题。)

我的想法是,这个问题是一个复杂的问题,您需要将复杂性转移到它所属的 DI 空间(因为这是所有组件和依赖项连接的地方,导致这种情况发生),以便您编写的任何新验证器不必意识到您想要注入的给定记录器的“循环依赖”问题。

我尝试解决此问题的一种方法是创建一个后备记录器。现在我绝不是说我的方法是事实上的标准,但它解决了问题,并且因为它应该只运行一次(因为它MyOptionsValidator被设置为单例),所以你不必担心运行时的任何性能影响。

我更改了执行此操作的代码:

public static IServiceCollection AddMyLib(this IServiceCollection services) =>
            services.AddSingleton<IValidateOptions<MyOptions>, MyOptionsValidator>();
Run Code Online (Sandbox Code Playgroud)

去做这个:

public static IServiceCollection AddMyLib(this IServiceCollection services) =>
            services.AddSingleton<IValidateOptions<MyOptions>, MyOptionsValidator>(
                sp => new MyOptionsValidator(CreateFallback<IValidateOptions<MyOptions>>()));

public static ILogger<T> CreateFallback<T>()
{
    return LoggerFactory.Create(x => x.AddConsole()).CreateLogger<T>();
}
Run Code Online (Sandbox Code Playgroud)

我不确定如何ILoggerFactory使用 .NET Core DI 基础设施注入辅助。也许您可以创建一个包装器类并使用 LoggerFactory 的嵌入式实例,然后在您想要使用后备记录器的任何地方解析该包装器类?

您必须设置一个单独的LoggerFactory实例,以确保不会暴露FileLogger可能导致问题的实例。这确实意味着您的AddMyLib扩展方法必须移动到您愿意拉入Microsoft.Extensions.Logging包(以及您希望在该过程中使用的任何记录器包)的地方,除非您可以使用我提到的包装器解决方案(使用课程)。

因此,如果您的应用程序配置不正确,它将记录配置错误,并且应用程序将停止运行,因为这MyOptionsValidator会引发异常。

记录配置错误并显示异常

但是如果您的应用程序配置正确......

应用程序运行愉快