0

I have a command line application written in C#. Here's some facts about the program that are relevant to my question:

  • The application has a "data directory" (e.g. ~/.config/myapp) where all user data is stored, including log files.
  • I use Autofac for dependency injection.
  • I use Serilog for my application logging (provided to classes as ILogger via DI).

There's a catch 22 in the initialization process that I am not sure how to elegantly resolve. Basically, there's code that needs ILogger injected (during object construction, since all of my DI happens at construction time) but that ILogger cannot be provided until it is registered.

So why isn't ILogger registered? Well, that's because I can't register it until I can tell the implementation of that interface where the log files will be written.

Where are the log files located? They are located in the app data directory (~/.config/myapp/logs).

So what's the issue? To know the path where the log files will go, I need to run through some code that calculates/figures out the path. The path can be manipulated in several ways (in order of precedence):

  1. A specific path can be provided via the user (e.g. command line argument)
  2. A path can be specified via an environment variable
  3. Lastly, the path defaults to some framework-defined value, e.g. ~/.config/myapp.

As you can imagine, all of this logic requires a few services which are themselves injected by DI. But how can I run this logic when my DI container hasn't been fully set up yet because the ILogger implementation can't be provided yet? There's a catch 22 / circular dependency here.

I've thought of a few solutions/workarounds:

  • Not outputting file logs if the logger is not set up for it yet. This is hard to do, mainly because Serilog enforces you to know all of this up front. In other words, you can't have a fully constructed ILogger and reconfigure it later to add a new file sink. Even if I could, there will be a "black out" period of no logs if it gets used.
  • Throw an exception if the logger gets used prior to it being ready, but this caused me to unnaturally implement classes and inject services, which is not very straightforward.
  • Use some mixture of Lazy<ILogger> but again this sort of contributes to the previous point: This makes things unnatural, error-prone, rigid, and confusing.

What is the right way to handle the order of operations here? My gut tells me that all of this app data directory setup logic should happen prior to my composition root setup, but that would be messy without DI available.

1
  • Not sure why my question is getting downvoted. I feel like I put a lot of effort into this question. Jun 27, 2022 at 13:43

2 Answers 2

4

Inject a factory that can create the ILogger.

eg. (pseudo code)

Services.Register<ILogger>(() => {
   //get required services from service container
   //compute path
   return new Logger(path);
}
4
  • 1
    Terse, but correct. Jun 26, 2022 at 17:33
  • What should I do if the compute path logic needs the logger? For example, to output a message showing the calculated application data directory for debugging purposes later? Jun 26, 2022 at 17:36
  • 2
    then your problem isn't to do with DI. But presumably you can create the computePath with an ILogger implementation which doesn't need a path
    – Ewan
    Jun 26, 2022 at 17:41
  • @void.pointer - you can have your compute path create the logger, use it for its own purposes, and then return it, so that the factory code above becomes return /*computePath*/(). And if you don't want it to depend on the logger (e.g., to allow for unit testing), you can (manually) inject a lambda into compute path that creates an ILogger of your choice, that compute path will internally call. E.g. Func<string, ILoger>, takes a path, calls the Logger constructor. In your tests, you can inject a lambda that returns a mock logger. Jun 26, 2022 at 19:30
1

Computing the path should not require the logger. Since this is a command line application, send the logging output to the standard output. Consider using System.Diagnostics.Debug.WriteLine(string) in this case. I really wonder what good a logger does when initializing the path used by the logger. If anything goes wrong in this phase of the application, throw an exception, catch it in Main(string[]) and exit the command line application with an error code/non-zero exit code.

The closer your code is to the beginning of the call stack, the simpler it's dependencies must be. In this case, the only dependency should be the host in which the application is running.

2
  • 2
    Yes. I do not see the point of insisting on having path find failures routed through your logger. Besides being pointless it is impossible. Suppose you run into an issue finding that path, you construct an error message and you do have an ILogger available. Where is it going to store that message? Jun 26, 2022 at 18:57
  • I've hit something similar in my own code, and the point of using a logger to display errors early on was formatting - I wanted uniform formatting for errors which happen before the logger is initialized. And for initialization I need to know the log level, which is set in configuration. In the end, since a configuration error means the program dies soon after I ended up with parsing it first.
    – jaskij
    Jun 27, 2022 at 19:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.