Circular dependency exception when creating a custom logger relying on options with a options validator relying on a logger

I followed @Olaf Svenson's recipe for replicating the problem, which I have been able to replicate. I then tried @Martin's approach by making the MyOptionsValidator aware that it needs a ILoggerFactory but it still resulted in a circular dependency scenario (somehow).

I think there should be a way to log unhandled exceptions so that you don't need to log anything in your MyOptionsValidator but rather let it return a failure result which would result in an exception being thrown and that be logged instead. But for Worker Services this seems to be an issue? Let's assume we cannot do this, then look at the solution I provided below...

(UPDATE: You actually don't even need to do this below but it was a cool challenge nonetheless. Don't log within your Validator. This will prevent the unnecessary complexity. The normal unhandled exception logging process will kick in and actually log to the other loggers when your logger's config is invalid. Super simple and very effective. Now you can have all kinds of loggers take care of this concern for you.)

My thinking is that this problem is a complicated one and you need to move the complexity to the DI space where it belongs (since that's where all the components and dependencies are wired up that caused this to happen) so that any new Validator that you write will not have to be aware of this "circular dependency" problem of a given logger that you want to inject.

The one method I tried to address this with is to create a Fall-back logger. Now I'm in no way saying my approach is the defacto standard but it solved the problem and since it should only run once (since the MyOptionsValidator is setup as a singleton) you don't have to worry about any performance hits at runtime.

I changed the code that did this:

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

To do this:

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

I am not sure how to inject a secondary ILoggerFactory using the .NET Core DI infrastructure. Maybe you could create a wrapper class and use an embedded instance of a LoggerFactory and then resolve that wrapper class everywhere you would like to use the Fall-back logger?

You have to setup a separate LoggerFactory instance to make sure you don't expose the FileLogger that can cause the problem. This does mean that your AddMyLib extension method would have to move somewhere where you are happy to pull in the Microsoft.Extensions.Logging package (and whatever logger package you wish to use in the process) unless you can make use of the wrapper solution I mentioned (using an abstraction of course).

So if your app is incorrectly configured, it will log the configuration error and the app will stop running since the MyOptionsValidator causes an exception to be raised.

Logging the configuration error and displaying the exception

But if your app is correctly configured...

App is running happily


Look at this from a logical standpoint: if your logging configuration is invalid, you can't log using that configuration, period, so there's no use to introduce the circular dependency: the loggers created by your ILoggingFactory won't be able to log, because of the invalid configuration.

So if you want to log that your logging configuration is invalid, look at how other logging frameworks do that. First, decide for yourself whether you consider this a fatal state. Do you not want your application to run in this scenario? In other words: should an invalid logging configuration prevent the program from running, or should it run and not log?

Either let it throw an exception, so the configuration error leaks to the console or event log, or do it as log4net and the likes: have a default, fail-safe logging mechanism, like logging to console or an opt-in logger-error-logfile.

I'd opt for the latter, so let your logger fail silently, and add an optional setting like logErrorLogFile, to which your logger configuration can log its log configuration error, and have a separate DI flow for that (or hardcode it).

And if that fails, for example when the file is not writable, it can throw an exception. Or not.

Tags:

C#

.Net Core