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:
- 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. - Class name is used to distinguish the context in the log, but explicitly passing class name seems not necessary
- It’s not possible to mock the
ILog
interface when it’s a static member variable in unit tests. - It’s not friendly to advanced scenarios. Assume the
SomeComponent
is a reusable class that different instances have differentContext
, it’s natural to also write theContext
into log to help locate source of the bugs. UsingILog
directly the above way requires passingContext
to the logger wheneverILog.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
}
ILogger
implementation can be easily switched to another logging library without touch the client codes.- Name of the class being injected can be obtained by
Unity Container
during object construction, it’s transparent to the client codes. - Dependency injection based approach is easy to
mock
and unit test friendly. - Assume the
SomeComponent
class implements a common interfaceILogContext
, during dependency injection, we can create a special instance ofILogger
and pass the instance of the currentSomeComponent
, then this special logger would be responsible for writingContext
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 ofSomeComponent
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 ofUnity Container 4.0
and later, so if not specified, all of the codes in this blog is based on4.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 ofUnity 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));
}
但这样做的问题也是显而易见的,在大型应用程序中尤其需要避免:
- 引入了对
log4net
的显示依赖。虽然现实中整体替换日志库的场景不多,但一旦出现对开发者来说就是噩梦。 - 类名用于在日志中输出当前类信息,但是显示传递类名导致代码冗长。
- 静态成员变量无法通过
moq
或NSubstitue
之类的工具来mock
日志ILog
接口,不利于单元测试。 - 对高级使用场景不友好。假如上面的
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的类,甚至实例信息
}
ILogger
的实现容易替换为另外的类库而无需更改其余代码。- 类名可以由
Unity Container
在注入的时候通过上下文获取,无需对使用者可见。 - 易于
mock
接口,方便单元测试。 - 以不同的实例有不同的
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. 实现
核心逻辑由继承自BuilderStrategy
的LoggerStrategy
完成,代码非常简单。值得注意的是对ILogContext
的处理使用了unsafe
代码从当前BuilderContext
的Parent
属性获取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
-
Previous
Handle Task Exceptions Gracefully -
Next
Unity Container (2) Implementing the INotifyPropertyChanged Interface