I have what I believe to be a fairly simple problem in theory, yet in reality it proves to be a bit more hairy. Below I present the problem and the requirements as I see them as well as a few possible solutions. The solution I've settled on may or may not be correct, so I'm tossing out here to see what you guys think. By all means please post some feedback.
To have a common, consistent logging access across multiple assemblies (that I own) in an application.
To the right is a picture of the assembly dependency diagram.
I could use Windsor's logging facility as suggested by Casey Charlton except that the solution provided only seemingly works for my top level assembly, ABCCompany.MVC.Web. I don't want my other assemblies to be aware that I'm using an IoC Container. I especially don't want the assemblies to know I'm bound to a particular IoC container (in this case I'm using Castle's Windsor container). I've emailed Casey about this and we are going to try to talk this through. I'm interested to hear what his thoughts are given that on Castle's site it says about the logging facility (emphasis mine):
The logging facility provides a seamless way to add logging capabilities to your application. There are two levels of integration. Allow your classes to receive an ILogger instance for logging support Allow you to ask for an ILoggerFactory instance to provide logging support to classes that are not managed by Windsor.
The logging facility provides a seamless way to add logging capabilities to your application. There are two levels of integration.
While I wait to talk with Casey I have to discount this solution since I cannot see past the first assembly on how this would work and therefore cannot use this as my solution of choice.
This is what I've come up with so far. Please comment if you see issues.
I have set up a static class inside of the ABCCompany.Framework assembly from which all methods could call for Logging purposes. This satisfies the requirements above which states that I do not want constructor dependencies nor public setters on all classes. This solution however does mean that every assembly much also ship with the ABCCompany.Framework assembly and all of it's dependent assemblies. I'm okay with this until I hear a reason that this is bad. I view this much like when I do work with Castle that in order to get any behavior I have to bring Castle.Core along to the party.
I liked Casey's solution (Solution #1) except that I cannot see past it working in one and only one assembly. I thought I could modify his solution by simply having a static class, which any assembly can reference and call. By default the Logger would be set up with a NullLogger. The Logging class would have a method available to change the underlying logger. It would then be incumbent upon the application layer to change the logger if they actually wanted logging. In my app this is the first thing that happens, the NullLogger is replaced by an actual logger by calling Log.SetLogger().
As you can see from the code below I've followed much of Casey's example.
1: private static ILogger logger;
2:
3: public static ILogger Logger
4: {
5: get
6: {
7: if (logger == null)
8: {
9: logger = NullLogger.Instance;
10: }
11: return logger;
12: }
13: }
14:
15: public static void SetLogger(ILogger value)
16: {
17: logger = value;
18: }
I do have a dependency on the Castle.Core assembly (due to usage of types in Castle.Core.Logging). However none of the classes using the Log static class have to reference anything to do with Castle since I've have methods which pass through to every method in ILogger (see example below):
1: public static void Debug(string message)
2: {
3: Logger.Debug(message);
4: }
5:
6: public static void Debug(string message, Exception exception)
7: {
8: Logger.Debug(message,exception);
9: }
10:
11: public static void Debug(string format, params object[] args)
12: {
13: Logger.Debug(format, args);
14: }
15:
16: public static void DebugFormat(string format, params object[] args)
17: {
18: Logger.DebugFormat(format, args);
19: }
20:
21: public static void DebugFormat(Exception exception, string format, params object[] args)
22: {
23: Logger.DebugFormat(exception,format, args);
24: }
25:
26: public static void DebugFormat(IFormatProvider formatProvider, string format, params object[] args)
27: {
28: Logger.DebugFormat(formatProvider, format, args);
29: }
30:
31: public static void DebugFormat(Exception exception, IFormatProvider formatProvider, string format, params object[] args)
32: {
33: Logger.DebugFormat(exception, formatProvider, format, args);
34: }
The usage pattern becomes quite simple in that any class that references ABCCompany.Framework can use the ABCCompany.Framework.Logging.Log class. No external references needs
The reason I chose to bind myself at the framework level to Castle's ILogger interface and not something like ILog (log4net) was because ILogger is an abstraction over the ILog interface. Castle also provides some concrete implementations of ILogger, one of which is for log4net
As far as testing is concerned, the solution imposes no restrictions on testing. In my testing assemblies my logging calls are swallowed by the null logger. If I want to ensure the logger is called in my testing (interaction based tests), I can mock an instance of ILogger and call Log.SetLogger() and set up expectations on the mock ILogger.
As I've mulled over this solution now for about a day it feels pretty solid. As I go through the requirements above they are all satisfied. However I can only see as far as my current knowledge so I'm hoping that if you see issues with the above that you'll let me know.
This blog contains the thoughts and discoveries of Tim Barcz, a technologist with a interests in computer programming technologies.