PAYATHON 2026

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 签名。

备注:内容仅供参考。