导读:本期聚焦于书生创作的《C#中Scrutor怎么实现程序集扫描注册?一文讲透依赖注入批量注册技巧》,敬请观看详情。.NET项目里服务一多,手动在Startup中逐个AddScoped会非常繁琐。Scrutor这个库提供了基于程序集扫描的批量注册能力,可以按类型、接口、生命周期等规则自动完成依赖注入配置。本文从Scrutor的安装和基本用法讲起,详细介绍按接口约定扫描、装饰器模式注册、生命周期控制等核心功能,并给出扫描策略设计、程序集拆分、性能注意事项等实践经验,帮助你在大型项目中规范地组织依赖注入代码,减少重复配置,提升项目可维护性。

为什么需要程序集扫描注册

在传统的ASP.NET Core项目中,每新增一个服务类,都要打开Startup或者Program文件,手动添加一行类似services.AddScoped<IOrderService, OrderService>()的代码。项目小的时候没什么感觉,但当一个解决方案里有几十个模块、上百个服务接口时,这个注册文件会膨胀到几百行,每次加服务都要改公共配置文件,既容易遗漏,也容易产生合并冲突。

更麻烦的是,手动注册违背了模块化设计的初衷。理想状态下,一个业务模块应该自成一体,新增或移除模块时不需要修改全局配置。程序集扫描注册就是为此而生的方案:通过约定(比如接口以I开头、实现类以Service结尾),让容器在启动时自动扫描指定程序集,把符合规则的类型批量注册进去,代码量和出错率都大幅下降。

Scrutor是.NET生态中最流行的扫描注册库,它以扩展方法的形式构建在微软自带的DependencyInjection之上,API流畅、语义清晰。它不是要替代内置容器,而是在内置容器的能力之上补充批量注册和装饰器等高级特性,因此几乎没有学习成本和迁移风险。

C#中Scrutor怎么实现程序集扫描注册?一文讲透依赖注入批量注册技巧

Scrutor的安装与基础用法

通过NuGet安装Scrutor非常简单,执行以下命令即可,它对.NET Core 3.1以后的所有版本都有良好支持:

dotnet add package Scrutor

最基础的用法是按类型扫描。假设项目中有多个以Service结尾的实现类,希望把所有非抽象的公共类自动注册为自身类型的瞬时服务,可以这样写:

using Scrutor;
using Microsoft.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        services.Scan(scan => scan
            .FromAssemblyOf<OrderService>()   // 从OrderService所在程序集扫描
            .AddClasses(classes => classes.Where(t => t.Name.EndsWith("Service")))
            .AsSelf()                          // 以自身类型注册
            .WithTransientLifetime()           // 瞬时生命周期
        );
        return services;
    }
}

这段代码会在Program.cs中通过builder.Services.AddApplicationServices()调用后生效。注意FromAssemblyOf<T>是按某个类型定位程序集的写法,也可以用FromAssemblies传入多个程序集,或者用FromCallingAssemblyFromApplicationDependencies等方式。扫描范围越小,启动速度越快,这一点在后面还会强调。

按接口约定匹配注册

实际项目中最常见的场景是接口和实现类成对出现,比如IOrderService对应OrderServiceIUserService对应UserServ�ice这样的命名约定。Scrutor提供了AsMatchingInterface方法来自动完成这种匹配:

services.Scan(scan => scan
    .FromAssemblyOf<IOrderService>()
    .AddClasses(classes => classes.AssignableTo<IService>()) // 可选:先过滤基类
    .AsMatchingInterface((type, builder) =>
        builder.InAssemblyOf(type))        // 在实现类所在程序集找对应接口
    .WithScopedLifetime());

AsMatchingInterface的默认约定是去掉实现类名前的I前缀:接口IOrderService匹配实现OrderService。如果你的命名约定不同,可以使用带委托参数的重载,自定义接口匹配逻辑。另一个常用选项是AsImplementedInterfaces,它会把类注册到它实现的所有接口上,适合一个类实现多个服务接口的情况。

如果同一个接口存在多个实现,需要决定是全部注册还是保留最后一个。Scrutor默认采用容器标准的“后注册覆盖前注册”行为,也可以通过UsingRegistrationStrategy明确指定策略:RegistrationStrategy.Skip表示遇到已注册的就跳过,RegistrationStrategy.Append表示全部追加(配合IEnumerable<T>注入可以拿到所有实现),RegistrationStrategy.Throw则会在重复时抛异常,便于及早发现配置错误。

services.Scan(scan => scan
    .FromAssemblyOf<OrderService>()
    .AddClasses()
    .AsImplementedInterfaces()
    .UsingRegistrationStrategy(RegistrationStrategy.Skip) // 已存在则跳过
    .WithScopedLifetime());

生命周期、过滤与装饰器进阶用法

生命周期控制非常直观,WithSingletonLifetimeWithScopedLifetimeWithTransientLifetime分别对应单例、作用域和瞬时。如果不同服务需要不同生命周期,可以进行多次Scan,每次用不同的过滤条件和生命周期,互不干扰:

// 无状态的计算服务用单例
services.Scan(scan => scan
    .FromAssemblyOf<Calculator>()
    .AddClasses(c => c.AssignableTo<ICalculator>())
    .AsImplementedInterfaces()
    .WithSingletonLifetime());

// 依赖数据库上下文的仓储用Scoped
services.Scan(scan => scan
    .FromAssemblyOf<OrderRepository>()
    .AddClasses(c => c.AssignableTo<IRepository>())
    .AsImplementedInterfaces()
    .WithScopedLifetime());

过滤条件除了AssignableTo(可赋值给某类型)和自定义Where谓词,还有NotInNamespaceOf<T>InNamespaceOf<T>这类按命名空间过滤的方法,可以把 Infrastructure 层和 Application 层的服务分开注册,让边界更清晰。此外AddClasses默认会排除抽象类、接口和编译器生成的类型,通常不必额外操心。

Scrutor还有一个杀手级功能是装饰器注册。比如想给所有查询服务加上日志或缓存装饰,不需要手写一堆包装类注册代码:

services.AddScoped<IOrderService, OrderService>();

// 给IOrderService套上缓存装饰器
services.Decorate<IOrderService>((inner, provider) =>
    new CachedOrderService(inner, provider.GetRequiredService<IMemoryCache>()));

装饰器按注册顺序层层包裹,先注册的在最内层,非常适合实现横切关注点,比如审计、重试、缓存,而业务类保持纯粹。

实践中的注意事项与建议

第一是扫描范围的控制。扫描整个应用程序域会显著拖慢启动速度,大型项目建议按模块拆分程序集,每个模块提供一个自己的AddXxxModule扩展方法,内部只扫描本模块程序集,由组合根统一调用。这样既加快了扫描,也让模块的依赖注册职责内聚,移除模块时只删一行调用。

第二是要提防反射扫描的固有局限。程序集扫描依赖类型在编译期可见,如果你使用了程序集动态加载或AOT裁剪(比如.NET的NativeAOT),反射扫描可能找不到类型或被裁剪掉,这类场景建议退回显式注册,或使用源生成器方案。另外,强命名程序集、程序集版本冲突也可能导致扫描异常,排查时可以先缩小扫描范围定位问题程序集。

第三是约定要明确且稳定。扫描注册的本质是把“约定”当成契约,一旦有人命名不规范,比如接口没有加I前缀,注册就会静默失败,问题往往到运行时才暴露。建议配合单元测试验证关键服务能否正常解析,例如遍历所有接口断言容器能解析出实现,把约定验证自动化:

[Fact]
public void AllServices_ShouldBeResolvable()
{
    var provider = new ServiceCollection()
        .AddApplicationServices()
        .BuildServiceProvider();

    var orderService = provider.GetRequiredService<IOrderService>();
    Assert.NotNull(orderService);
}

总结来说,Scrutor用极小的成本解决了依赖注入配置膨胀的问题。合理设计命名约定、按模块划分扫描边界、用装饰器处理横切逻辑,再辅以启动时的解析验证测试,就能让项目的依赖注入配置既简洁又可靠,把开发者从繁琐的手工注册中彻底解放出来。

Scrutor程序集扫描依赖注入修改时间:2026-09-03 01:03:54

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260903/49249.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。