简介
老 .NET 项目里经常能看到这样的代码:
csharp
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 的服务:
csharp
public class OrderService
{
private readonly OrderRepository repository = new OrderRepository();
public void Create(int orderId)
{
repository.Save(orderId);
}
}
OrderService 直接依赖 OrderRepository,还自己负责创建它。这样的代码有两个明显问题:
text
业务类依赖具体实现
业务类还承担对象创建职责
改成构造函数注入:
csharp
public class OrderService
{
private readonly IOrderRepository repository;
public OrderService(IOrderRepository repository)
{
this.repository = repository;
}
}
此时 OrderService 不关心仓储由谁创建,也不关心最终使用数据库还是内存。StructureMap 负责把依赖关系补齐:
text
IOrderService
↓
OrderService
↓ 需要 IOrderRepository
OrderRepository
↓ 需要 IConnectionFactory
SqlConnectionFactory
这个过程叫自动装配(Auto-Wiring):容器读取构造函数参数,递归解析每个依赖,最后组装出完整对象图。
安装 StructureMap
StructureMap 最后一个稳定版本是 4.7.1,发布时间较早。维护老项目时可以固定版本安装:
shell
dotnet add package StructureMap --version 4.7.1
或者在 Visual Studio 的 NuGet 包管理器中安装:
powershell
Install-Package StructureMap -Version 4.7.1
老 ASP.NET MVC 5 项目还可能看到这些适配包:
text
StructureMap.MVC5
StructureMap.WebApi2
它们解决的是框架入口和控制器解析的集成问题,不等同于 StructureMap 核心包。ASP.NET Core 项目则通常使用内置 Microsoft.Extensions.DependencyInjection,或者迁移到 Lamar、Autofac 等仍在维护的容器。
第一个 Demo:注册接口并自动注入
下面是一个单文件控制台示例。示例中的 OrderService 没有手动创建仓储,容器会根据构造函数自动完成注入。
csharp
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);
输出:
text
开始创建订单:1001
保存订单:1001
最重要的注册语句是:
csharp
_.For<IOrderRepository>().Use<OrderRepository>();
含义很简单:解析 IOrderRepository 时,创建 OrderRepository。IOrderService 的注册则告诉容器可以创建 OrderService;创建过程中发现构造函数需要 IOrderRepository,再回头解析仓储。
具体类型通常可以自动解析,但显式注册能让配置意图更清楚:
csharp
_.For<OrderService>().Use<OrderService>();
Registry:把容器配置单独组织起来
所有配置都写在 new Container 里,项目变大后会很快失控。StructureMap 推荐使用 Registry,把一组相关注册集中到一个类中:
csharp
using StructureMap;
public sealed class OrderRegistry : Registry
{
public OrderRegistry()
{
For<IOrderRepository>().Use<OrderRepository>();
For<IOrderService>().Use<OrderService>();
}
}
创建容器时加载注册表:
csharp
using var container = new Container(new OrderRegistry());
var service = container.GetInstance<IOrderService>();
service.Create(1001);
多个模块可以分别创建注册表:
text
AppRegistry
├── OrderRegistry
├── UserRegistry
└── InfrastructureRegistry
也可以在总注册表中组合它们:
csharp
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
csharp
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 后会释放在这个范围内创建的可释放对象:
csharp
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 和默认约定:
csharp
public sealed class AppRegistry : Registry
{
public AppRegistry()
{
Scan(_ =>
{
_.TheCallingAssembly();
_.WithDefaultConventions();
});
}
}
默认约定通常会把:
text
IUserService → UserService
IOrderRepository → OrderRepository
自动注册起来。也可以只扫描指定程序集或指定类型所在程序集:
csharp
Scan(_ =>
{
_.AssemblyContainingType<OrderService>();
_.WithDefaultConventions();
});
扫描并不等于"所有类型都自动可用"。抽象类、接口、命名不符合约定的实现,仍然需要显式配置。生产项目最好限制扫描范围,并通过容器验证尽早发现漏注册问题。
注册同一接口的多个实现
AddAllTypesOf<T>() 可以把一个程序集中的多个实现注册为同一个插件类型:
csharp
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>();
});
}
}
需要指定顺序或名称时,也可以逐个注册:
csharp
For<INotificationSender>().Use<EmailSender>().Named("email");
For<INotificationSender>().Use<SmsSender>().Named("sms");
命名实例:同一个接口对应多种实现
支付渠道、消息发送渠道、文件存储实现,经常需要同时存在。可以使用 Named 区分:
csharp
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");
如果业务代码频繁出现字符串名称,说明选择逻辑可能应该单独封装成工厂:
csharp
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 属性注入:
csharp
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
下面把注册、自动装配、单例日志和嵌套容器放在同一个示例中:
csharp
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);
}
对象关系如下:
text
IOrderService
└── OrderService
├── ILogger → ConsoleLogger(Singleton)
└── IOrderRepository → MemoryOrderRepository
调用方只需要依赖 IOrderService,不需要知道 OrderService、MemoryOrderRepository 和 ConsoleLogger 的创建过程。
容器诊断:别等运行到某个接口才发现漏注册
StructureMap 提供了几个很有价值的诊断 API:
csharp
using var container = new Container(new AppRegistry());
// 查看容器当前拥有的注册信息
Console.WriteLine(container.WhatDoIHave());
// 启动阶段验证配置是否可以构建
container.AssertConfigurationIsValid();
AssertConfigurationIsValid() 适合放到启动检查或集成测试中。构造函数依赖没有注册、多个实现没有明确默认值等问题,可以在应用启动时暴露,而不是等到某个请求首次访问时才失败。
常见异常信息通常与这些原因有关:
- 接口没有任何实现注册;
- 多个实现都存在,但没有默认实现;
- 构造函数参数是基础类型,容器不知道该传什么值;
- 循环依赖,例如
A依赖B,B又依赖A; - 扫描范围不正确,目标程序集没有被扫描到。
基础类型参数一般需要显式提供:
csharp
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 项目通常直接使用:
csharp
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
如果确实需要 StructureMap 的扫描、约定和诊断能力,Lamar 是更自然的迁移方向。两者 API 思路相近,但不能假设所有扩展包和宿主适配代码都能零修改迁移。
常见坑
把 StructureMap 容器注入业务类
csharp
public class BadOrderService
{
private readonly IContainer container;
public BadOrderService(IContainer container)
{
this.container = container;
}
}
这会让业务类和具体容器绑定。更好的方式是直接声明真实依赖:
csharp
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 的核心可以归纳成四步:
text
Registry 注册规则
↓
Container 保存配置
↓
GetInstance 解析对象
↓
生命周期负责复用与释放
最常用的代码是:
csharp
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 架构的重要工具,不是新项目的首选容器。新系统优先选择仍在维护的方案,旧系统则先保证生命周期、释放和配置诊断正确。
参考资料: