Unity Container (1) Injecting type aware ILogger

Advanced Unity Container usages

Posted by eagleboost on April 4, 2021

Many large scale applications adopt the concept of IoC, it’s especially useful in terms of component de-coupling and writing reusable codes. Among all of the IoC containers on the market, Unity Container is no doubt on top of the list for its rich features and flexibilities. This blog series would focus on introducing some Unity Container advanced usages based on scenarios of real projects to help reduce duplicated works in developments.

1. The problem

Logging is one of the features that almost every application cannot miss. Libraries like log4net are already written so developers don’t need to reinvent the wheels. Those logging support libraries are usually also easy to use, take log4net as an example, we just need to call LogManager.GetLogger to get an ILog instance and start to use it, like the SomeComponent class below.

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace log4net
{
  public class LogManager
  {
    public static ILog GetLogger(string name);
    public static ILog GetLogger(Type type);
  }
}

public class SomeComponent
{
  private static ILog Logger = LogManager.GetLogger(typeof(SomeComponent));
}

While it’s straightforward enough to get started, the problems are also obvious enough to notice and should be avoided in large scale aplications:

  1. Explicit dependencies to log4net. Although it’s not common to replace a logging library entirely, but it would be nightmares to developers once it needs to happen.
  2. Class name is used to distinguish the context in the log, but explicitly passing class name seems not necessary
  3. It’s not possible to mock the ILog interface when it’s a static member variable in unit tests.
  4. It’s not friendly to advanced scenarios. Assume the SomeComponent is a reusable class that different instances have different Context, it’s natural to also write the Context into log to help locate source of the bugs. Using ILog directly the above way requires passing Context to the logger whenever ILog.Info is called, this can easily cause massive duplicated and tedious codes. Using extension methods can reduce some duplications but it’s not good enough.

This blog demonstrate a Unity Container based solution to use the most concise codes to solve the above problems.

2. Analysis

#1 can be easily fixed by introducing a new interface, for example Prism provides an ILogFacade interface for the same purpose. We’ll define an ILogger interface for demo purpose. The goal is to use dependency injection to simplify the codes:

1
2
3
4
5
6
7
8
9
10
public interface ILogger
{
  void Info(string msg);
}

public class SomeComponent
{
  [Dependency]
  public ILogger Logger { get; set; }   //The Logger instance already contain name of the SomeComponent class, or even the instance of SomeComponent 
}
  1. ILogger implementation can be easily switched to another logging library without touch the client codes.
  2. Name of the class being injected can be obtained by Unity Container during object construction, it’s transparent to the client codes.
  3. Dependency injection based approach is easy to mock and unit test friendly.
  4. Assume the SomeComponent class implements a common interface ILogContext, during dependency injection, we can create a special instance of ILogger and pass the instance of the current SomeComponent, then this special logger would be responsible for writing Context to log so the client codes can completely be freed from such works.
1
2
3
4
public interface ILogContext
{
  object Context { get; }
}

Note 1:#4 can only be applied to property injection, not constructor injection because the instance of SomeComponent is not created yet.

Note 2:Unity Container provides flexible customization via Extension that allows developers create custom codes to control creation of the objects. But the customization has changed a lot in the version of Unity Container 4.0 and later, so if not specified, all of the codes in this blog is based on 4.0 and later versions.

We know that there’re several stages of the object creation in Unity Container, below table is quoted from 《Dependency Injection With Unity》:

Stage Description
Setup The first stage. By default, nothing happens here.
TypeMapping The second stage. Type mapping takes place here.
Lifetime The third stage. The container checks for a lifetime manager.
PreCreation The fourth stage. The container uses reflection to discover the constructors, properties, and methods of the type being resolved.
Creation The fifth stage. The container creates the instance of the type being resolved.
Initialization The sixth stage. The container performs any property and method injection on the instance it has just created.
PostInitialization The last stage. By default, nothing happens here.

For the ILogger, we can place a BuilderStrategy at the PreCreation stage to create ILogger instance based on certain conditions, for example, write the log to Debug Output or disk file, passing class name etc.

One article on Microsoft Docs Case study provided by Dan Piessens shows how to pass class name to ILogger based on earlier versions of Unity Container, I have enriched the missing details of the article and put the full workable sample on github.

3. Implementaions

Core logic is implemented by the LoggerStrategy derived from BuilderStrategy. Please note that unsafe is used the when handling ILogContext to retrieve the instance of the object being injected from the Parent property of the BuilderContext. The reason of unsafe is that somehow the Parent Context is converted and stored as IntPtr, so we have to reverse the process unsafely.

1
2
3
4
5
6
7
var context = new BuilderContext
{
  ......
#if !NET40
  Parent = new IntPtr(Unsafe.AsPointer(ref thisContext))
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class LoggerStrategy : BuilderStrategy
{
  public override void PreBuildUp(ref BuilderContext context)
  {
    base.PreBuildUp(ref context);

    if (typeof(ILogger).IsAssignableFrom(context.Type))
    {
      context.Existing = CreateLogger(ref context);
      context.BuildComplete = true;
    }
  }

  private static ILogger CreateLogger(ref BuilderContext context)
  {
    var result = LoggerManager.GetLogger(context.DeclaringType);

    ////Try creating ContextLogger if the declaring type implements ILogContext
    if (typeof(ILogContext).IsAssignableFrom(context.DeclaringType))
    {
      if (context.Existing == null && context.Parent != IntPtr.Zero)
      {
        unsafe
        {
          var pContext = Unsafe.AsRef<BuilderContext>(context.Parent.ToPointer());
          ////pContext.Existing is null when ILogger is being injected to the ctor
          if (pContext.Existing is ILogContext logContext)
          {
            ////If ILogger is being injected to a property, we create a ContextLogger to wrap the real logger
            result = new ContextLogger(logContext, result);
          }
        }
      }
    }

    return result;
  }
}

LoggerManager.GetLogger returns an instance of ILogger based on the given type, LoggerManager can internally cache the ILogger instances per type as well as switch implementations. Below codes show a way of switching to TraceLogger when the debugger is attached to write the log to Debug Output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class LoggerManager
{
  private static readonly ConcurrentDictionary<Type, ILogger> Loggers = new ConcurrentDictionary<Type, ILogger>();
  
  public static ILogger GetLogger(Type type)
  {
    return Loggers.GetOrAdd(type, CreateLogger);
  }

  private static ILogger CreateLogger(Type type)
  {
    if (Debugger.IsAttached)
    {
      return (ILogger) Activator.CreateInstance(typeof(TraceLogger<>).MakeGenericType(type));
    }
    
    return (ILogger) Activator.CreateInstance(typeof(Log4NetLogger<>).MakeGenericType(type));
  }
}

ContextLogger simply write ILogContext.Context to log along with the log message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public sealed class ContextLogger : ILogger
{
  private readonly ILogger _logger;
  private readonly ILogContext _logContext;

  public ContextLogger(ILogContext logContext, ILogger logger)
  {
    _logContext = logContext ?? throw new ArgumentNullException(nameof(logContext));
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
  }

  public void Info(string msg)
  {
    LogCore(msg);
  }
  
  private void LogCore(string msg)
  {
    _logger.Info($"{_logContext.Context} : {msg}");
  }
}

Please visit github for more details.

在大型应用程序中使用IoC对于解耦和开发可重用代码大有益处,选择一个强大的IoC容器则可能使得应用程序开发如虎添翼。Unity Container无疑是众多IoC容器中的佼佼者。本系列旨基于实际项目介绍一些Unity Container的高级使用方式,帮助减少开发中的一些重复劳动。

1. 问题

记录日志几乎可算是每个应用程序都必不可少的功能,相关的支持库也不少,比如.net环境下最常用的log4net。这些日志库通常使用起来也非常方便,添加引用,再简单配置一下config即可。

log4net为例,一般是调用LogManager来获取一个ILog实例,比如下面的SomeComponent类。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace log4net
{
  public class LogManager
  {
    public static ILog GetLogger(string name);
    public static ILog GetLogger(Type type);
  }
}

public class SomeComponent
{
  private static ILog Logger = LogManager.GetLogger(typeof(SomeComponent));
}

但这样做的问题也是显而易见的,在大型应用程序中尤其需要避免:

  1. 引入了对log4net的显示依赖。虽然现实中整体替换日志库的场景不多,但一旦出现对开发者来说就是噩梦。
  2. 类名用于在日志中输出当前类信息,但是显示传递类名导致代码冗长。
  3. 静态成员变量无法通过moqNSubstitue之类的工具来mock日志ILog接口,不利于单元测试。
  4. 对高级使用场景不友好。假如上面的SomeComponent是一个可重用的类,但不同的实例有不同的Context,记录日志的时候很自然需要记录该Context来帮助定位错误来源。直接使用ILog就需要在每个调用ILog.Info的地方把Context拼接为字符串的一部分传入,这容易导致大量重复而冗长的代码。引入扩展方法可以在一定程度上减少重复,但并不优美。

本文给出一种基于Unity Container的解决方案,以最简洁的代码解决上述问题。

2. 分析

引入一个新的接口即可避免对log4net的显示依赖,比如Prism库就有一个ILogFacade接口。为简单起见,本文定义一个ILogger接口,其成员可仿照ILogFacade接口,不再赘述。我们的目标是通过依赖注入ILogger实例的方式来简化代码,如下所示:

1
2
3
4
5
6
7
8
9
10
public interface ILogger
{
  void Info(string msg);
}

public class SomeComponent
{
  [Dependency]
  public ILogger Logger { get; set; }   //该实例已经包含了SomeComponent的类,甚至实例信息
}
  1. ILogger的实现容易替换为另外的类库而无需更改其余代码。
  2. 类名可以由Unity Container在注入的时候通过上下文获取,无需对使用者可见。
  3. 易于mock接口,方便单元测试。
  4. 以不同的实例有不同的Context为例,假设SomeComponent实现了一个基础接口ILogContext,依赖注入的时候一旦检测到ILogContext则可以构建一个特殊的ILogger并传入当前SomeComponent的实例,那么把Context一并输出到日志的工作则可以由该ILogger完成,对使用者来说完全透明。
1
2
3
4
public interface ILogContext
{
  object Context { get; }
}

注 1:#4由于与实例相关,因此仅适用于属性依赖注入的情形,因为注入到构造函数的时候SomeComponent的实例尚未创建完成。

注 2:Unity Container作为老牌的IOC容器,通过Extension对于组件创建的全过程提供了灵活的可定制化功能使得开发者可以插入代码来控制组件的创建。Unity Container 4.0以后的版本在定制化方面有较大的改动,使得定制化的实现也大不相同。如无特殊说明,本文所有代码均以4.0以后的版本为例。

我们知道Unity Container中组件的创建有不同的stage,下面的表格来自《Dependency Injection With Unity》:

Stage Description
Setup The first stage. By default, nothing happens here.
TypeMapping The second stage. Type mapping takes place here.
Lifetime The third stage. The container checks for a lifetime manager.
PreCreation The fourth stage. The container uses reflection to discover the constructors, properties, and methods of the type being resolved.
Creation The fifth stage. The container creates the instance of the type being resolved.
Initialization The sixth stage. The container performs any property and method injection on the instance it has just created.
PostInitialization The last stage. By default, nothing happens here.

对于ILogger来说可以在PreCreation阶段插入一个BuilderStrategy来根据一定条件实例化一个日志类,比如是把日志输出到文件还是调试器的Debug Output,被创建的类名信息等等。

Microsoft Docs上有一篇Case study provided by Dan Piessens给出了基于Unity Container早期版本一个实现,用于把当前被创建的类名传递给ILogger,我把原文略去的一些细节补充之后放在了github上。

3. 实现

核心逻辑由继承自BuilderStrategyLoggerStrategy完成,代码非常简单。值得注意的是对ILogContext的处理使用了unsafe代码从当前BuilderContextParent属性获取ILogger被注入为属性的类的实例。unsafe的原因是Parent Context的引用不知为什么被转换为IntPtr来传递,所以也只能用unsafe方式转换过后来访问。

1
2
3
4
5
6
7
var context = new BuilderContext
{
  ......
#if !NET40
  Parent = new IntPtr(Unsafe.AsPointer(ref thisContext))
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class LoggerStrategy : BuilderStrategy
{
  public override void PreBuildUp(ref BuilderContext context)
  {
    base.PreBuildUp(ref context);

    if (typeof(ILogger).IsAssignableFrom(context.Type))
    {
      context.Existing = CreateLogger(ref context);
      context.BuildComplete = true;
    }
  }

  private static ILogger CreateLogger(ref BuilderContext context)
  {
    var result = LoggerManager.GetLogger(context.DeclaringType);

    ////如果declaring type实现了ILogContext,则创建一个ContextLogger
    if (typeof(ILogContext).IsAssignableFrom(context.DeclaringType))
    {
      if (context.Existing == null && context.Parent != IntPtr.Zero)
      {
        unsafe
        {
          var pContext = Unsafe.AsRef<BuilderContext>(context.Parent.ToPointer());
          ////如果ILogger是被依赖注入到构造函数,那么pContext.Existing就是null
          if (pContext.Existing is ILogContext logContext)
          {
            ////如果ILogger是被依赖注入到属性,那么pContext.Existing就是已经创建的被注入到对象实例。我们创建ContextLogger并把该对象引用以及实际做事的ILogger传入
            result = new ContextLogger(logContext, result);
          }
        }
      }
    }

    return result;
  }
}

LoggerManager.GetLogger根据给定类型返回一个ILogger的实例,LoggerManager可以包含按类型缓存ILogger实例以及切换不同实现的逻辑。比如下面的代码演示了当调试器加载的情况下使用TraceLogger从而把所有日志输出到Debug Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class LoggerManager
{
  private static readonly ConcurrentDictionary<Type, ILogger> Loggers = new ConcurrentDictionary<Type, ILogger>();
  
  public static ILogger GetLogger(Type type)
  {
    return Loggers.GetOrAdd(type, CreateLogger);
  }

  private static ILogger CreateLogger(Type type)
  {
    if (Debugger.IsAttached)
    {
      return (ILogger) Activator.CreateInstance(typeof(TraceLogger<>).MakeGenericType(type));
    }
    
    return (ILogger) Activator.CreateInstance(typeof(Log4NetLogger<>).MakeGenericType(type));
  }
}

ContextLogger则简单地把ILogContext.Context与日志信息拼接起来一并写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public sealed class ContextLogger : ILogger
{
  private readonly ILogger _logger;
  private readonly ILogContext _logContext;

  public ContextLogger(ILogContext logContext, ILogger logger)
  {
    _logContext = logContext ?? throw new ArgumentNullException(nameof(logContext));
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
  }

  public void Info(string msg)
  {
    LogCore(msg);
  }
  
  private void LogCore(string msg)
  {
    _logger.Info($"{_logContext.Context} : {msg}");
  }
}

完整实现请移步github