C#.NET StructureMap 从依赖注入到项目实战

简介

老 .NET 项目里经常能看到这样的代码:

var service = new OrderService(
    new OrderRepository(
        new SqlConnectionFactory()),
    new EmailNotifier());

对象少时,手动 new 很直观。对象关系变复杂后,创建代码会慢慢蔓延到控制器、业务类和启动代码中:实现类换了,要改很多处;测试时想替换成内存仓储,也要一路修改构造函数。

依赖注入容器做的事情并不神秘:集中保存“接口对应哪个实现”的规则,根据构造函数自动创建对象,再按照生命周期管理实例。

StructureMap 就是 .NET 历史较久的 IoC/DI 容器,特色是流畅的 Registry 配置、自动装配、程序集扫描和嵌套容器。

不过必须先说明版本背景:StructureMap 官方仓库已经标注项目 sunsetted,也就是停止新功能开发;官方建议新项目使用同作者后续的 Lamar。StructureMap 目前更适合维护遗留项目、阅读旧 ASP.NET MVC 项目,或理解 Lamar 的历史 API。新项目不建议为了“尝鲜”引入它。

本文以 StructureMap 4.7.1 为例,从一个可以运行的控制台 Demo 开始,逐步讲清注册、解析、自动装配、生命周期、扫描、命名实例、集合注入、嵌套容器和诊断方法。

StructureMap 解决的到底是什么问题

先看一个没有 DI 的服务:

public class OrderService
{
    private readonly OrderRepository repository = new OrderRepository();

    public void Create(int orderId)
    {
        repository.Save(orderId);
    }
}

OrderService 直接依赖 OrderRepository,还自己负责创建它。这样的代码有两个明显问题:

业务类依赖具体实现
业务类还承担对象创建职责

改成构造函数注入:

public class OrderService
{
    private readonly IOrderRepository repository;

    public OrderService(IOrderRepository repository)
    {
        this.repository = repository;
    }
}

此时 OrderService 不关心仓储由谁创建,也不关心最终使用数据库还是内存。StructureMap 负责把依赖关系补齐:

IOrderService
    ↓
OrderService
    ↓ 需要 IOrderRepository
OrderRepository
    ↓ 需要 IConnectionFactory
SqlConnectionFactory

这个过程叫自动装配(Auto-Wiring):容器读取构造函数参数,递归解析每个依赖,最后组装出完整对象图。

安装 StructureMap

StructureMap 最后一个稳定版本是 4.7.1,发布时间较早。维护老项目时可以固定版本安装:

dotnet add package StructureMap --version 4.7.1

或者在 Visual Studio 的 NuGet 包管理器中安装:

Install-Package StructureMap -Version 4.7.1

老 ASP.NET MVC 5 项目还可能看到这些适配包:

StructureMap.MVC5
StructureMap.WebApi2

它们解决的是框架入口和控制器解析的集成问题,不等同于 StructureMap 核心包。ASP.NET Core 项目则通常使用内置 Microsoft.Extensions.DependencyInjection,或者迁移到 Lamar、Autofac 等仍在维护的容器。

第一个 Demo:注册接口并自动注入

下面是一个单文件控制台示例。示例中的 OrderService 没有手动创建仓储,容器会根据构造函数自动完成注入。

using StructureMap;

public interface IOrderRepository
{
    void Save(int orderId);
}

public sealed class OrderRepository : IOrderRepository
{
    public void Save(int orderId)
    {
        Console.WriteLine($"保存订单:{orderId}");
    }
}

public interface IOrderService
{
    void Create(int orderId);
}

public sealed class OrderService : IOrderService
{
    private readonly IOrderRepository repository;

    public OrderService(IOrderRepository repository)
    {
        this.repository = repository;
    }

    public void Create(int orderId)
    {
        Console.WriteLine($"开始创建订单:{orderId}");
        repository.Save(orderId);
    }
}

using var container = new Container(_ =>
{
    _.For<IOrderRepository>().Use<OrderRepository>();
    _.For<IOrderService>().Use<OrderService>();
});

var service = container.GetInstance<IOrderService>();
service.Create(1001);

输出:

开始创建订单:1001
保存订单:1001

最重要的注册语句是:

_.For<IOrderRepository>().Use<OrderRepository>();

含义很简单:解析 IOrderRepository 时,创建 OrderRepository。IOrderService 的注册则告诉容器可以创建 OrderService;创建过程中发现构造函数需要 IOrderRepository,再回头解析仓储。

具体类型通常可以自动解析,但显式注册能让配置意图更清楚:

_.For<OrderService>().Use<OrderService>();

Registry:把容器配置单独组织起来

所有配置都写在 new Container 里,项目变大后会很快失控。StructureMap 推荐使用 Registry,把一组相关注册集中到一个类中:

using StructureMap;

public sealed class OrderRegistry : Registry
{
    public OrderRegistry()
    {
        For<IOrderRepository>().Use<OrderRepository>();
        For<IOrderService>().Use<OrderService>();
    }
}

创建容器时加载注册表:

using var container = new Container(new OrderRegistry());

var service = container.GetInstance<IOrderService>();
service.Create(1001);

多个模块可以分别创建注册表:

AppRegistry
├── OrderRegistry
├── UserRegistry
└── InfrastructureRegistry

也可以在总注册表中组合它们:

public sealed class AppRegistry : Registry
{
    public AppRegistry()
    {
        IncludeRegistry<OrderRegistry>();
        IncludeRegistry<UserRegistry>();
    }
}

注册代码集中在组合根,业务类只依赖接口,不应该在构造函数里保存 Container,更不应该在业务方法里调用 GetInstance<T>()。后者属于服务定位器写法,会让依赖变得隐蔽,也会让单元测试更麻烦。

生命周期:Transient、Singleton 和 Nested Container

生命周期决定实例什么时候创建、是否复用、什么时候释放。StructureMap 的默认生命周期是 Transient,这一点和部分默认使用 Singleton 或显式 Scoped 的容器不同。

生命周期 行为 常见用途
Transient 每次从普通容器请求时创建新实例 无状态服务、轻量组件
Singleton 整个根容器中复用一个实例 配置、缓存、线程安全的共享服务
ContainerScoped 在当前容器范围内复用 配合嵌套容器模拟请求或事务范围
AlwaysUnique 每次请求都强制创建新实例 必须完全隔离的对象

Singleton

public sealed class ConsoleLogger : ILogger
{
    public void Write(string message)
    {
        Console.WriteLine(message);
    }
}

public interface ILogger
{
    void Write(string message);
}

var container = new Container(_ =>
{
    _.For<ILogger>().Use<ConsoleLogger>().Singleton();
});

var first = container.GetInstance<ILogger>();
var second = container.GetInstance<ILogger>();

Console.WriteLine(ReferenceEquals(first, second)); // True
container.Dispose();

单例服务必须考虑线程安全和状态污染。把请求级数据放进单例,容易造成并发请求之间互相影响。

Nested Container:请求级或事务级范围

StructureMap 的嵌套容器适合短生命周期操作。嵌套容器继承根容器的注册关系,退出 using 后会释放在这个范围内创建的可释放对象:

public sealed class RequestContext : IDisposable
{
    public Guid Id { get; } = Guid.NewGuid();

    public void Dispose()
    {
        Console.WriteLine($"释放请求上下文:{Id}");
    }
}

var root = new Container(_ =>
{
    _.For<RequestContext>().Use<RequestContext>().ContainerScoped();
});

using (var request = root.GetNestedContainer())
{
    var context1 = request.GetInstance<RequestContext>();
    var context2 = request.GetInstance<RequestContext>();

    Console.WriteLine(ReferenceEquals(context1, context2)); // True
}

root.Dispose();

嵌套容器可以对应一次 HTTP 请求、一次消息消费或一次数据库事务。根容器负责应用级资源,嵌套容器负责范围内资源,边界结束时统一释放。

需要注意一个容易混淆的细节:StructureMap 官方文档中,普通根容器下的 Transient 是每次请求创建新实例;在嵌套容器中,默认瞬态对象会由该嵌套容器跟踪并在范围内复用。具体行为应结合目标项目版本和生命周期配置验证,不能直接套用其他 DI 容器的经验。

自动扫描:让注册代码从几十行变成几行

项目里有大量“一接口对应一实现”的类型时,可以使用 Scan 和默认约定:

public sealed class AppRegistry : Registry
{
    public AppRegistry()
    {
        Scan(_ =>
        {
            _.TheCallingAssembly();
            _.WithDefaultConventions();
        });
    }
}

默认约定通常会把:

IUserService → UserService
IOrderRepository → OrderRepository

自动注册起来。也可以只扫描指定程序集或指定类型所在程序集:

Scan(_ =>
{
    _.AssemblyContainingType<OrderService>();
    _.WithDefaultConventions();
});

扫描并不等于“所有类型都自动可用”。抽象类、接口、命名不符合约定的实现,仍然需要显式配置。生产项目最好限制扫描范围,并通过容器验证尽早发现漏注册问题。

注册同一接口的多个实现

AddAllTypesOf<T>() 可以把一个程序集中的多个实现注册为同一个插件类型:

public interface INotificationSender
{
    void Send(string message);
}

public sealed class EmailSender : INotificationSender
{
    public void Send(string message) => Console.WriteLine($"邮件:{message}");
}

public sealed class SmsSender : INotificationSender
{
    public void Send(string message) => Console.WriteLine($"短信:{message}");
}

public sealed class NotificationRegistry : Registry
{
    public NotificationRegistry()
    {
        Scan(_ =>
        {
            _.TheCallingAssembly();
            _.AddAllTypesOf<INotificationSender>();
        });
    }
}

需要指定顺序或名称时,也可以逐个注册:

For<INotificationSender>().Use<EmailSender>().Named("email");
For<INotificationSender>().Use<SmsSender>().Named("sms");

命名实例:同一个接口对应多种实现

支付渠道、消息发送渠道、文件存储实现,经常需要同时存在。可以使用 Named 区分:

public sealed class ChannelRegistry : Registry
{
    public ChannelRegistry()
    {
        For<INotificationSender>().Use<EmailSender>().Named("email");
        For<INotificationSender>().Use<SmsSender>().Named("sms");
    }
}

using var container = new Container(new ChannelRegistry());

var email = container.GetInstance<INotificationSender>("email");
var sms = container.GetInstance<INotificationSender>("sms");

email.Send("订单已创建");
sms.Send("验证码:9527");

如果业务代码频繁出现字符串名称,说明选择逻辑可能应该单独封装成工厂:

public sealed class NotificationSenderFactory
{
    private readonly IContainer container;

    public NotificationSenderFactory(IContainer container)
    {
        this.container = container;
    }

    public INotificationSender Create(string channel)
    {
        return container.GetInstance<INotificationSender>(channel);
    }
}

更推荐把容器调用限制在组合根或工厂里,避免把 StructureMap API 扩散到整个业务层。

属性注入:能用,但不应作为首选

StructureMap 支持 Setter 属性注入:

public sealed class ReportService
{
    public ILogger? Logger { get; set; }

    public void Export()
    {
        Logger?.Write("开始导出报表");
    }
}

var container = new Container(_ =>
{
    _.For<ILogger>().Use<ConsoleLogger>();
    _.For<ReportService>().Use<ReportService>()
        .Setter<ILogger>().Is<ConsoleLogger>();
});

属性注入适合遗留框架对象、可选依赖或无法改造构造函数的类型。普通业务服务优先构造函数注入,因为构造函数能清楚表达必需依赖,并且对象创建后处于完整状态。

一个更完整的订单 Demo

下面把注册、自动装配、单例日志和嵌套容器放在同一个示例中:

using StructureMap;

public interface ILogger
{
    void Info(string message);
}

public sealed class ConsoleLogger : ILogger
{
    public void Info(string message)
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
    }
}

public interface IOrderRepository
{
    string Find(int orderId);
}

public sealed class MemoryOrderRepository : IOrderRepository
{
    public string Find(int orderId) => $"订单-{orderId}";
}

public interface IOrderService
{
    void Handle(int orderId);
}

public sealed class OrderService : IOrderService
{
    private readonly ILogger logger;
    private readonly IOrderRepository repository;

    public OrderService(ILogger logger, IOrderRepository repository)
    {
        this.logger = logger;
        this.repository = repository;
    }

    public void Handle(int orderId)
    {
        var order = repository.Find(orderId);
        logger.Info($"处理 {order}");
    }
}

public sealed class AppRegistry : Registry
{
    public AppRegistry()
    {
        For<ILogger>().Use<ConsoleLogger>().Singleton();
        For<IOrderRepository>().Use<MemoryOrderRepository>();
        For<IOrderService>().Use<OrderService>();
    }
}

using var root = new Container(new AppRegistry());

using (var scope = root.GetNestedContainer())
{
    var service = scope.GetInstance<IOrderService>();
    service.Handle(2001);
}

对象关系如下:

IOrderService
  └── OrderService
      ├── ILogger → ConsoleLogger(Singleton)
      └── IOrderRepository → MemoryOrderRepository

调用方只需要依赖 IOrderService,不需要知道 OrderService、MemoryOrderRepository 和 ConsoleLogger 的创建过程。

容器诊断:别等运行到某个接口才发现漏注册

StructureMap 提供了几个很有价值的诊断 API:

using var container = new Container(new AppRegistry());

// 查看容器当前拥有的注册信息
Console.WriteLine(container.WhatDoIHave());

// 启动阶段验证配置是否可以构建
container.AssertConfigurationIsValid();

AssertConfigurationIsValid() 适合放到启动检查或集成测试中。构造函数依赖没有注册、多个实现没有明确默认值等问题,可以在应用启动时暴露,而不是等到某个请求首次访问时才失败。

常见异常信息通常与这些原因有关:

  • 接口没有任何实现注册;
  • 多个实现都存在,但没有默认实现;
  • 构造函数参数是基础类型,容器不知道该传什么值;
  • 循环依赖,例如 A 依赖 B,B 又依赖 A;
  • 扫描范围不正确,目标程序集没有被扫描到。

基础类型参数一般需要显式提供:

public sealed class FileExporter
{
    private readonly string directory;

    public FileExporter(string directory)
    {
        this.directory = directory;
    }
}

var container = new Container(_ =>
{
    _.For<FileExporter>().Use<FileExporter>()
        .Ctor<string>("directory").Is("/tmp/export");
});

配置字符串、连接串等外部值不适合直接散落在注册代码里,通常应先绑定配置对象,再通过工厂或构造函数传入。

StructureMap 与 ASP.NET Core 的关系

StructureMap 主要活跃于 ASP.NET MVC 5、Web API 2 和早期 .NET Core 过渡阶段。ASP.NET Core 自带 IServiceCollection,项目如果仍然选择 StructureMap,通常需要适配器把 StructureMap 容器接入 IServiceProvider。

维护旧项目时,应先确认:

  • 目标框架和 StructureMap 4.7.1 的兼容性;
  • 现有宿主是否已经使用 IServiceProvider;
  • 第三方中间件是否依赖微软默认容器的注册方式;
  • 请求作用域是否通过嵌套容器正确创建和释放;
  • 应用是否有迁移到 Lamar 或内置 DI 的计划。

新 ASP.NET Core 项目通常直接使用:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

如果确实需要 StructureMap 的扫描、约定和诊断能力,Lamar 是更自然的迁移方向。两者 API 思路相近,但不能假设所有扩展包和宿主适配代码都能零修改迁移。

常见坑

把 StructureMap 容器注入业务类

public class BadOrderService
{
    private readonly IContainer container;

    public BadOrderService(IContainer container)
    {
        this.container = container;
    }
}

这会让业务类和具体容器绑定。更好的方式是直接声明真实依赖:

public OrderService(IOrderRepository repository, ILogger logger)
{
    // 依赖清楚、测试时容易替换
}

误以为默认生命周期是 Singleton

StructureMap 默认使用 Transient。需要共享实例时显式写 .Singleton();需要请求级范围时使用嵌套容器或项目已经约定好的作用域配置。

扫描范围过大

扫描整个 AppDomain 可能把测试类、第三方类型或不应该暴露的实现一起注册。优先指定程序集、命名空间和过滤规则,并在启动时执行容器验证。

多实现没有默认值

同一个接口注册多个实现时,必须指定默认实现或使用名称解析。否则直接调用 GetInstance<T>() 可能因为无法判断默认对象而失败。

Singleton 持有范围对象

单例服务不能依赖请求级或嵌套容器级服务,否则容易把短生命周期对象“带”进长生命周期,产生状态串扰、资源无法及时释放等问题。

把装饰器、拦截器和 DI 容器混为一谈

StructureMap 负责对象注册和创建;装饰器、拦截器属于额外的扩展能力。跨项目接入前应确认对应包和版本,不要只看到某篇旧文章里的 API 就直接复制。

什么时候适合继续使用 StructureMap

适合继续使用的情况:

  • 正在维护基于 StructureMap 的稳定遗留系统;
  • 项目依赖已有的扫描、Registry、嵌套容器和诊断配置;
  • 当前目标是修复业务问题,不适合同时切换 DI 容器;
  • 已经有完整的容器配置测试和升级回滚方案。

不适合新引入的情况:

  • 新建 ASP.NET Core 服务;
  • 需要长期获得新 .NET 版本适配;
  • 需要活跃维护的扩展生态;
  • 团队没有维护旧容器和宿主适配代码的经验。

新项目可以优先评估内置 DI、Lamar 或 Autofac。选型重点不是 API 链式写法是否漂亮,而是版本支持、作用域语义、诊断能力、测试成本和团队维护能力。

总结

StructureMap 的核心可以归纳成四步:

Registry 注册规则
        ↓
Container 保存配置
        ↓
GetInstance 解析对象
        ↓
生命周期负责复用与释放

最常用的代码是:

public class AppRegistry : Registry
{
    public AppRegistry()
    {
        For<IOrderRepository>().Use<OrderRepository>();
        For<IOrderService>().Use<OrderService>();
    }
}

using var container = new Container(new AppRegistry());
var service = container.GetInstance<IOrderService>();

For<T>().Use<TImpl>() 解决显式注册,Scan() 和 WithDefaultConventions() 解决批量注册,Singleton() 与 ContainerScoped() 解决实例范围,GetNestedContainer() 解决请求或事务级资源管理,WhatDoIHave() 和 AssertConfigurationIsValid() 解决配置排查。

StructureMap 值得掌握,但定位应放准确:它是理解和维护老 .NET DI 架构的重要工具,不是新项目的首选容器。新系统优先选择仍在维护的方案,旧系统则先保证生命周期、释放和配置诊断正确。

参考资料:

posted on 2026-09-08 10:28  我是唐青枫  阅读(7)  评论(0)    收藏  举报