views:

1133

answers:

4

I have the following interface:

public interface ILogger
{
    void Debug(string message, params object[] values);
    void Info(string message, params object[] values);
    void Warn(string message, params object[] values);
    void Error(string message, params object[] values);
    void Fatal(string message, params object[] values);
}

and the following implementation:

public class Log4netLogger : ILogger
{
    private ILog _log;

    public Log4netLogger(Type type)
    {
        _log = LogManager.GetLogger(type);
    }

    public void Debug(string message, params object[] values)
    {
        _log.DebugFormat(message, values);
    }

    // other logging methods here...

}

My idea was to use structuremap to instantiate the Log4netLogger class with using the Type of the class that did the logging. However, I can't for the life of me figure out how to pass the type of the calling class to structuremap so that it can be passed to the constructor of the logging implementation. Any advice on how to do that (or a better way) would be most appreciated.

A: 

If the type parameter is context-specific, I don't think this is going to work as shown. If you need to pass something context specific in the constructor, you are likely going to have to create a factory interface and implementation that returns an instance of the ILogger:

public interface ILoggerFactory
{
    ILogger Create(Type type);   
}

public class LoggerFactory : ILoggerFactory
{
    public ILogger Create(Type type)
    {
        return new Log4netLogger(type);
    }
}

It might be possible to bootstrap StructureMap to supply the instance you want based on the type, but that assumes a limited number of types that you know in advance.

Phil Sandler
The need for factories when using structuremap can usually be eliminated by using an "ConstructedBy" with a lambda as the factory method. http://structuremap.sourceforge.net/InstanceExpression.htm#section13
KevM
A: 

I really need to get out of the habit of answering my own question, but for those who run across it, here's the answer.

return ObjectFactory.With(type).GetInstance<T>();

I actually have a wrapper to structuremap (to avoid exposing the structuremap dependency to my app) that looks like the following:

public static class ServiceManager
{
    public static T Get<T>()
    {
        return ObjectFactory.GetInstance<T>();
    }

    public static T Get<T>(Type type)
    {
        return ObjectFactory.With(type).GetInstance<T>();
    }
}

Any time in the code I need a logger, I call the following:

ServiceManager.Get<ILogger>(GetType()).Info("Logging page view...");
Chris
I actually don't think there is anything wrong with considering the logger a global service (as it is generally cross-cutting), but this does create a static dependency on your ServiceManager. I'm not saying your solution is wrong (far from it), but it sounds a little bit like it may tend toward the global service locator "anti-pattern". Essentially your dependency on ILogger (or ILoggerFactory) is (or may be) no longer explicit.
Phil Sandler
Interesting point Phil. I'm not sure what the alternative would be though (architectural recommendations on IoC are sparse, indeed). In my case, the class is an HttpHandler which needs access to the ILogger it is requesting from the ServiceManager. How else would I implement?
Chris
I absolutely agree on architecture recommendations being sparse, and your question IMHO is *the* most common hurdle people come across when they are getting started with a DI framework. Creating an abstract factory is the best answer I have come up with, and I often implement the factory using StructureMap (ObjectFactory.GetInstance) so that dependencies further down the chain can still be container-managed. This may not be *perfect*, but at least dependencies are kept explicit.
Phil Sandler
Hmmm... since the code is in an HttpHandler, I can't really use a factory for it. Maybe I should move the code to a different class and use the IoC container to load that class with the ILogger as an explicit dependency in the constructor. Would that be more appropriate?
Chris
I don't know that much about HttpHandlers. I'm guessing this class isn't/can't be manged by the container? If that's the case, and you're not looking at a deep dependency chain beyond this class, then you should probably ignore my comments and not belabor the design. :)
Phil Sandler
Just checking in to make sure you know about AutoWiring in structure map. http://codebetter.com/blogs/jeremy.miller/archive/2009/01/07/autowiring-in-structuremap-2-5.aspxService location while sometimes necessary is best avoided when possible. Let the container do your work for you via auto wiring and injection. Ctor > Setter > Service Location.
KevM
@Kevin, as I stated previously to Phil, the issue is that the class in question is an HttpHandler which is instantiated by ASP.NET directly, I have no control over the process, so I cannot inject structuremap into the pipeline to let it do the autowiring. However, what I did end up doing is moving the main logic to a class that accepts ILogger as a constructor arg and then calling ObjectFactory from the HttpHandler, allowing it to fill in the ILogger argument automatically. Best approach I could think of.
Chris
@Chris: In order to DI into an HttpHandler you can use a custom PageHandlerFactory. See http://aspnetresources.com/articles/ioc_and_di_with_web_forms.aspx for more info
PHeiberg
+5  A: 

We use a similar ILogger wrapper around log4net and typically use constructor injection. We use an interceptor as a factory method responsible for creating the Logger. Here is our typical registry for logging setup.

public class CommonsRegistry : Registry
{
    public CommonsRegistry()
    {
        For<ILogger>()
            .AlwaysUnique()
            .TheDefault.Is.ConstructedBy(s =>
            {
                if (s.ParentType == null)
                    return new Log4NetLogger(s.BuildStack.Current.ConcreteType);

                return new Log4NetLogger(s.ParentType);
            });

        var applicationPath = Path.GetDirectoryName(Assembly.GetAssembly(GetType()).Location);
        var configFile = new FileInfo(Path.Combine(applicationPath, "log4net.config"));
        XmlConfigurator.ConfigureAndWatch(configFile);
    }
}

The parent type null check is necessary when there are dependencies on concrete types.

The rest is optional log4net setup stuff.

One thing I do like about this setup is the ability to use a null loggers for unit testing.

KevM
Interesting, I'll have to try this. Thanks!
Chris
This is indeed interesting. I also saw your comment on my answer. Would this work if the ctor dependency was anything outside of the calling class's type? Would love to see a blog post on context-specific ctor dependencies, as this has to be the #1 most asked question about using a DI framework.
Phil Sandler
Also, I'm struggling with whether this solution makes the dependency less explicit. If construction really requires a factory, is it a good idea to "hide" the factory in configuration code, instead of making the dependency on an abstract factory explicit? (I mean this as a theoretical question, not necessarily as the best solution for Chris or for logging per se).
Phil Sandler
Not sure I understand your ctor question. When you put a dependency into your constructor StructureMap will make sure it gets injected into the instances of this class when they are created. This is true for each type down the graph of each type's dependencies. Jeremy just posted about a new feature nested containers which give you control over contextual dependencies: http://codebetter.com/blogs/jeremy.miller/archive/2010/01/06/how-dovetail-uses-structuremap-with-nhibernate.aspx
KevM
A factory of this nature is really configuration so it feels right having it live in the registry where we put most of the our application's configuration. If you are worried create a static factory method. When an IoC container is in play it feels silly having overly simple factory classes describing how to create types. as that is what IoC is for.
KevM
A: 

This is what i've done to get the correct type getting passed into Log4Net, without having to manually create each logger, in each class. Thank dependency injection :)

Note: IoC == StructureMap.

namespace Insert.Your.Funky.Called.Medina.Name.Ass.Space.Here
{
    public class ServicesRegistry : Registry
    {
        public ServicesRegistry()
        {
            // Because we're using Log4Net .. which requires some configuration information ... lets load that now.
            // This trick with AppDomain was stollen from NLog :)
            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config");
            if (!File.Exists(path))
            {
                throw new FileNotFoundException("The log4net.config file was not found -> ru-roh!!! Please make sure it exists. Maybe check the path? Current file we tried to load is: " + path);
            }

            log4net.Config.XmlConfigurator.Configure(new FileInfo(path));

            Scan(x =>
                {
                    x.TheCallingAssembly();
                }
            );

            // First, grab the current stackTrace.
            var stackTrace = new System.Diagnostics.StackTrace().GetFrames();

            // Now, inject a logging service whenever requested. Pass in the type
            // which is the calling method's class. The first slot (ie. [0]) is usually
            // the ctor of the logging service class. so we jump up one frame
            // and use the class type, of that method.
            For<ILoggingService>().Use<Log4NetLogging>()
                .Ctor<Type>("type").Is(stackTrace.Length >= 2 ? 
                    stackTrace[1].GetMethod().DeclaringType : 
                    stackTrace[0].GetMethod().DeclaringType);            

             // Add any other specific injection rules here.
        }
    }
}

namespace Insert.Another.Funky.Called.Medina.Name.Ass.Space.Here
{
    public class Log4NetLogging : ILoggingService
    {
        private static ILog Log4Net { get; set; }

        public Log4NetLogging(Type type)
        {
            Log4Net = log4net.LogManager.GetLogger(type);
        }

        // blah blah .. all the logging interfaces, coded against Log4Net.
    }
}
Pure.Krome