I know that Service Locator pattern is out of favour. When you have something like Global.Kernel.Get<IService>()
in your code the consultant will detect a code smell. So where possible, we should be using constructor/property or method injection.
Now let's consider a common task of logging with Ninject. There is a couple of extensions such as ninject.extensions.logging and Ninject.Extensions.Logging.Log4net
that "just work". You reference them in your project and then whenever you inject ILogger
you get a wrapped instance of log4net logger that is named after the type you are injecting into. You don't even have a dependency on log4net anywhere in you code, just in the place where you provide it with the configuration. Great!
But what if you need a logger in a class that is not created in a DI container? How do you get it there? It can't be auto injected because the class is created outside of the DI, you can do Global.Kernel.Inject(myObject) because it would be wrong to depend on the DI not in the composition root as explained in the article linked above, so what options do you really have?
Below is my attempt at it, but I do not like it because it feels mighty awkward. I'm actually injecting ILoggerFactory instead of ILogger to the class that creates outside-of-DI object and then pass the ILoggerFactory to the outside-of-DI object in the constructor.
Another way of achieving the same would be to introduce the log4net dependency into the outside-of-DI class, but that also feels backward.
How would you solve this?
And here is a code example:
using System.Threading;
using Ninject;
using Ninject.Extensions.Logging;
namespace NjTest
{
// This is some application configuration information
class Config
{
public string Setting1 { get; set; }
}
// This represent messages the application receives
class Message
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public string Param3 { get; set; }
}
// This class represents a worker that collects and process information
// in our example this information comes partially from the message and partially
// driven by configuration
class UnitCollector
{
private readonly ILogger _logger;
// This field is not present in the actual class it's
// here just to give some exit condition to the ProcessUnit method
private int _defunct;
public UnitCollector(string param1, string param2, ILoggerFactory factory)
{
_logger = factory.GetCurrentClassLogger();
// param1 and param2 are used during processing but for simplicity we are
// not showing this
_logger.Debug("Creating UnitCollector {0},{1}", param1, param2);
}
public bool ProcessUnit(string data)
{
_logger.Debug("ProcessUnit {0}",_defunct);
// In reality the result depends on the prior messages content
// For simplicity we have a simple counter here
return _defunct++ < 10;
}
}
// This is the main application service
class Service
{
private readonly ILogger _logger;
private readonly Config _config;
// This is here only so that it can be passed to UnitCollector
// and this is the crux of my question. Having both
// _logger and _loggerFactory seems redundant
private readonly ILoggerFactory _loggerFactory;
public Service(ILogger logger, ILoggerFactory loggerFactory, Config config)
{
_logger = logger;
_loggerFactory = loggerFactory;
_config = config;
}
//Main processing loop
public void DoStuff()
{
_logger.Debug("Hello World!");
UnitCollector currentCollector = null;
bool lastResult = false;
while (true)
{
Message message = ReceiveMessage();
if (!lastResult)
{
// UnitCollector is not injected because it's created and destroyed unknown number of times
// based on the message content. Still it needs a logger. I'm here passing the logger factory
// so that it can obtain a logger but it does not feel clean
// Another way would be to do kernel.Get<ILogger>() inside the collector
// but that would be wrong because kernel should be only used at composition root
// as per http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
currentCollector = new UnitCollector(message.Param2, _config.Setting1,_loggerFactory);
}
lastResult = currentCollector.ProcessUnit(message.Param1);
//message.Param3 is also used for something else
}
}
private Message ReceiveMessage()
{
_logger.Debug("Waiting for a message");
// This represents receiving a message from somewhere
Thread.Sleep(1000);
return new Message {Param1 = "a",Param2 = "b",Param3 = "c"};
}
}
class Program
{
static void Main()
{
log4net.Config.XmlConfigurator.Configure();
IKernel kernel = new StandardKernel();
kernel.Bind<Config>().ToMethod(ctx => new Config {Setting1 = "hey"});
Service service = kernel.Get<Service>();
service.DoStuff();
}
}
}
App.Config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.13.0" newVersion="1.2.13.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>