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):
- A specific path can be provided via the user (e.g. command line argument)
- A path can be specified via an environment variable
- 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.
-
Not sure why my question is getting downvoted. I feel like I put a lot of effort into this question.void.pointer– void.pointer06/27/2022 13:43:25Commented Jun 27, 2022 at 13:43
2 Answers 2
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);
}
-
1Terse, but correct.Greg Burghardt– Greg Burghardt06/26/2022 17:33:05Commented 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?void.pointer– void.pointer06/26/2022 17:36:55Commented Jun 26, 2022 at 17:36 -
2then your problem isn't to do with DI. But presumably you can create the computePath with an ILogger implementation which doesn't need a pathEwan– Ewan06/26/2022 17:41:07Commented 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 becomesreturn /*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 intocompute path
that creates an ILogger of your choice, thatcompute 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.Filip Milovanović– Filip Milovanović06/26/2022 19:30:20Commented Jun 26, 2022 at 19:30
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.
-
2Yes. 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?Martin Maat– Martin Maat06/26/2022 18:57:12Commented 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– jaskij06/27/2022 19:34:26Commented Jun 27, 2022 at 19:34
Explore related questions
See similar questions with these tags.