Let's say I have this service abstraction exposed from a library.
public interface INavigator
{
ImmutableList<IPageViewModel> Entries { get; }
void NavigateForward(IPageViewModel page);
void RemoveEntries(int indices);
void NavigateBack();
}
The library also exposes a default implementation that takes in an ILogger
as dependency.
Each method uses the logger.
public class DefaultNavigator : INavigator
{
private ILogger _logger;
public DefaultNavigator(ILogger logger)
{
_logger = logger;
}
public void NavigateForward(IPageViewModel page)
{
// [...]
_logger.LogInformation("Navigated to {ViewModelName}.", page.Name);
}
// [...]
}
The library also exposes extensions methods on INavigator
to add functionality, in the same fashion as System.Linq
(where the IEnumerable<T>
interface is very simple and all the core logic comes from the extension methods).
In our case, it makes sense to implement some logs in those extension methods.
public static class NavigatorExtensions
{
public static void ReturnToRoot(INavigator navigator)
{
var logger = ???
// [...]
logger.LogInformation("Returned to root of navigation stack.");
}
}
Here's the question: Where should we get the ILogger
from?
What we currently have
We currently can access the IServiceProvider
statically, which allows us to get an ILogger
from anywhere. However, we want to move away from that public static
pattern because we want to have multiple IServiceProvider
instances in the same process.
Options I'm considering
1. Expose ILogger
from INavigator
Does it make sense though?
public interface INavigator
{
ImmutableList<IPageViewModel> Entries { get; }
void NavigateForward(IPageViewModel page);
void RemoveEntries(int indices);
void NavigateBack();
// This isn't related to navigation.
ILogger Logger { get; }
}
- It doesn't feel quite right because that new member doesn't have anything to do with navigation.
2. Create a new interface that exposes an ILogger and soft cast in the extension method
// Not the best name, but I'm trying to avoid conflicts with the existing ILoggerProvider.
public interface ILoggerSource
{
ILogger Logger { get; }
}
The extension method would do something like this:
public static class NavigatorExtensions
{
public static void ReturnToRoot(INavigator navigator)
{
var logger = (navigator as ILoggerSource)?.Logger ?? NullLogger.Instance;
// [...]
logger.LogInformation("Returned to root of navigation stack.");
}
}
- If this was the proper solution, I think
ILoggerSource
would already exist in MS libraries. May be it does, but I didn't see it? - New implementations of
INavigator
don't have any clue that logs from extensions methods will not to work unless they also implementILoggerSource
.
3. I'm open to other options :)
Conclusion
As of writing this, I think option 1 is currently the lesser evil because it doesn't hide functionality, but my engineer mind is not satisfied.
INavigator
could be more explicit by using C# 8 nullables and having theILogger
be nullable (or not) so that extension methods would know whether to null check or assume a non-null value. That way implementers could use eithernull
orNullLogger
depending on the definition.