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

简介

.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 时,创建 OrderRepositoryIOrderService 的注册则告诉容器可以创建 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,不需要知道 OrderServiceMemoryOrderRepositoryConsoleLogger 的创建过程。

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

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

csharp 复制代码
using var container = new Container(new AppRegistry());

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

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

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

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

  • 接口没有任何实现注册;
  • 多个实现都存在,但没有默认实现;
  • 构造函数参数是基础类型,容器不知道该传什么值;
  • 循环依赖,例如 A 依赖 BB 又依赖 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 架构的重要工具,不是新项目的首选容器。新系统优先选择仍在维护的方案,旧系统则先保证生命周期、释放和配置诊断正确。

参考资料:

相关推荐
fujisheng6612 小时前
FUI 编译期装配实践:从反射注册到 Source Generator
c#·unity3d
智码看视界2 小时前
.NET 10 推理大模型TensorSharp 3.3.0 部署实测:纯.NET推理引擎反超llama.cpp 1.5倍,DFlash2提速62%
c#·.net·llama.cpp·.net 10·tensorsharp·本地大模型推理·开源推理引擎
格林威3 小时前
C# 图像异步落盘存储:基于Channel 配合 ArrayPool 实现异步落盘
开发语言·人工智能·数码相机·机器学习·计算机视觉·c#·视觉检测
软件黑马王子3 小时前
11.缓存池优化:窗口布局
开发语言·前端框架·c#
格林威3 小时前
C# 图像使用AVX2指令集:使用OpenCvSharp实现字节图像解压缩速度和map_image算子速度提升
开发语言·图像处理·人工智能·计算机视觉·c#·视觉检测·工业相机
软件黑马王子4 小时前
12.缓存池优化:对象上限
开发语言·前端框架·c#
唐青枫17 小时前
别把日志、权限写进业务方法:C#.NET 动态代理从原理到实战
c#·.net
风云17 小时前
Vane.Dispatch 1.0.0 发布:一个与容器、传输层零耦合的 .NET 服务分发引擎
微服务·mvc·.net·ndf·vane.dispatch