C# 中如何使用 DI 容器注册和组合 Decorator?
Decorator 可以与 DI 配合使用。对于示例中的 Microsoft.Extensions.DependencyInjection 容器,可以将底层 ConsoleWriter 按具体类型注册,再通过工厂构造装饰器,并将最外层对象注册为 IWriter。
GetRequiredService<IWriter>() 不会返回 IEnumerable<IWriter>。同一服务类型有多个注册时,解析单个服务会使用最后一个注册;只有解析 IEnumerable<IWriter> 时,才会获得全部注册的实现。
为什么原来的注册不能形成装饰链?
原来的代码是:
services.AddTransient<IWriter, ConsoleWriter>();
services.AddTransient<IWriter, ConsoleWriterDecorator>();
这两行分别注册了两个实现,并没有表达“第二个实现包装第一个实现”的关系。
解析 IWriter 时,容器会选择最后注册的 ConsoleWriterDecorator,而它的构造函数又需要一个 IWriter:
public ConsoleWriterDecorator(IWriter writer)
容器再次解析 IWriter 时,仍会选择 ConsoleWriterDecorator,因此产生循环依赖。容器不会自动退回到前一个注册。
原问题中的工厂也有同样的问题:如果正在构造 IWriter,又在工厂内部调用 GetRequiredService<IWriter>(),就会重新进入该工厂,造成递归解析。
即使改为解析 IEnumerable<IWriter>,集合中仍然包含依赖 IWriter 的装饰器,也不能靠这种方式建立包装关系。
通过工厂明确指定底层实现
沿用问题中的 IWriter、ConsoleWriter 和 ConsoleWriterDecorator,将原来的两条注册替换为:
using Microsoft.Extensions.DependencyInjection;
services.AddTransient<ConsoleWriter>();
services.AddTransient<IWriter>(sp =>
new ConsoleWriterDecorator(
sp.GetRequiredService<ConsoleWriter>()));
两个解析入口分别用于:
ConsoleWriter用于取得底层实现。IWriter用于取得已经完成包装的对象。
在应用已有的作用域或服务提供程序中解析:
IWriter writer = serviceProvider.GetRequiredService<IWriter>();
writer.Write("Pootis");
按照问题中 Write 方法的逻辑,输出为:
Hello, Im decorator - Pootis
此时的调用关系是:
调用方 → ConsoleWriterDecorator → ConsoleWriter
调用方只依赖 IWriter,无需知道背后有几层包装。
构造函数还有其他依赖怎么办?
如果 ConsoleWriter 增加了其他构造函数依赖,只要这些依赖已经注册,容器解析 ConsoleWriter 时就会继续解析它们。
如果 ConsoleWriterDecorator 也有其他依赖,可以使用 ActivatorUtilities.CreateInstance,显式传入需要包装的 IWriter,其余构造函数参数交给容器解析:
services.AddTransient<ConsoleWriter>();
services.AddTransient<IWriter>(sp =>
{
IWriter inner = sp.GetRequiredService<ConsoleWriter>();
return ActivatorUtilities.CreateInstance<ConsoleWriterDecorator>(
sp,
inner);
});
例如,装饰器以后增加一个已经注册的辅助服务作为构造函数参数,使用这种工厂通常就不需要逐个手动解析新增参数。
这要求额外依赖能够从容器解析,构造函数也能够被明确匹配。ActivatorUtilities 负责创建对象,不会自动推断装饰链;内层对象仍需由工厂明确传入。
如何组合多个 Decorator?
可以继续在工厂中逐层包装。例如,再增加一个将文本转换为大写的装饰器:
public class UpperCaseWriterDecorator : IWriter
{
private readonly IWriter _writer;
public UpperCaseWriterDecorator(IWriter writer)
{
_writer = writer;
}
public void Write(string text)
{
_writer.Write(text.ToUpperInvariant());
}
}
将 IWriter 的工厂替换为:
services.AddTransient<ConsoleWriter>();
services.AddTransient<IWriter>(sp =>
{
IWriter writer = sp.GetRequiredService<ConsoleWriter>();
writer = ActivatorUtilities.CreateInstance<ConsoleWriterDecorator>(
sp,
writer);
writer = ActivatorUtilities.CreateInstance<UpperCaseWriterDecorator>(
sp,
writer);
return writer;
});
调用顺序是:
调用方
→ UpperCaseWriterDecorator
→ ConsoleWriterDecorator
→ ConsoleWriter
调用 Write("Pootis") 时,外层先把输入转换为 POOTIS,内层再添加前缀。按照这段代码的逻辑,输出为:
Hello, Im decorator - POOTIS
交换两层的包装顺序后,前缀也会参与大写转换。组合多个 Decorator 时,需要根据行为执行顺序安排内外层。
使用时需要注意什么?
同一组示例应只保留一份最终的 IWriter 注册工厂。不要把这些替代方案全部追加到原来的错误注册后面,否则集合解析等场景仍可能遇到有问题的注册。
示例使用 AddTransient,每次解析都会构造一条新的装饰链。改用其他生命周期时,要同时考虑整条链的依赖,避免让 singleton 长期持有 scoped 服务。
如果链上的对象实现了 IDisposable,还需要明确各层资源由谁释放。尤其是通过 ActivatorUtilities 手动创建的内层对象,不能假定它们都会被容器单独跟踪和释放。
原代码中的 GetReqiuredService 应拼写为 GetRequiredService;参数声明 string args[] 应写成 string[] args。如果该方法用作传统 C# 程序入口,还应使用合适的 static Main 签名。
备注:内容仅供参考。
