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.