首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >彻底搞懂 .NET DI 的三种注册姿势:普通、泛型与工厂函数

彻底搞懂 .NET DI 的三种注册姿势:普通、泛型与工厂函数

原创
作者头像
步步为营DotNet
发布2026-08-07 09:35:53
发布2026-08-07 09:35:53
1150
举报

彻底搞懂 .NET DI 的三种注册姿势:普通、泛型与工厂函数

在 Modern .NET(尤其是 Minimal API)开发中,依赖注入(DI)是我们每天都要打交道的工具。

Program.cs 里,你肯定经常写 builder.Services.AddScoped(...)。但你可能会发现,这个 API 提供了好几种不同的写姿势:有时候传两个类型,有时候传一个泛型,有时候里面还带个 sp => 的 Lambda 表达式。

这些注册方式到底有什么区别?分别适用于什么场景?

本文将基于 .NET Minimal API,用最通俗易懂的方式为你拆解 DI 的三种核心注册姿势:普通注册泛型注册工厂函数注册

一、 预备知识:我们要解决什么问题?

假设我们正在开发一个用户管理系统

我们需要一个日志服务 ILoggerService 及其实现类 ConsoleLogger,以及一个处理用户业务的 UserService

C#

代码语言:javascript
复制
 // 1. 日志接口与实现
 public interface ILoggerService
 {
     void Log(string message);
 }
 ​
 public class ConsoleLogger : ILoggerService
 {
     public void Log(string message) => Console.WriteLine($"[LOG]: {message}");
 }
 ​
 // 2. 用户服务接口与实现
 public interface IUserService
 {
     void CreateUser(string name);
 }
 ​
 public class UserService : IUserService
 {
     private readonly ILoggerService _logger;
 ​
     // 依赖注入:构造函数需要一个 ILoggerService
     public UserService(ILoggerService logger)
     {
         _logger = logger;
     }
 ​
     public void CreateUser(string name)
     {
         _logger.Log($"User {name} created!");
     }
 }

我们要做的,就是把这些服务注册到 builder.Services(服务容器)里,让 Minimal API 的 Route Handler 在运行时能够自动拿到它们。

二、 第一种:泛型注册(最常用、最优雅)

“最推荐的日常写法,类型安全,代码干净。”

1. 怎么写?

在 Minimal API 中,泛型注册是我们 90% 的场景下都在写的形式。使用尖括号 <Service, Implementation> 明确指定接口类型实现类型

C#

代码语言:javascript
复制
 var builder = WebApplication.CreateBuilder(args);
 ​
 // 【泛型注册】:将 ILoggerService 映射到 ConsoleLogger
 builder.Services.AddSingleton<ILoggerService, ConsoleLogger>();
 ​
 // 【泛型注册】:将 IUserService 映射到 UserService
 builder.Services.AddScoped<IUserService, UserService>();
 ​
 var app = builder.Build();
 ​
 // 在 Minimal API 中直接注入使用
 app.MapPost("/users", (string name, IUserService userService) =>
 {
     userService.CreateUser(name);
     return Results.Ok($"Created {name}");
 });
 ​
 app.Run();

2. 特点与适用场景

  • 语法简洁:没有任何多余的类型转换或动态反射代码。
  • 编译期安全:如果你填入的实现类没有实现该接口,编译器会直接报错,根本无法通过编译。
  • Native AOT 友好:C# 编译器在编译时就能完全明确类型关系,不会被 AOT 裁剪工具误删。

三、 第二种:普通注册(基于 Type 的非泛型注册)

“适合反射、批量注册和框架级封装。”

1. 怎么写?

普通注册不使用尖括号 <>,而是直接通过 C# 的 typeof(...) 传入 Type 类型的参数。

C#

代码语言:javascript
复制
 var builder = WebApplication.CreateBuilder(args);
 ​
 // 【普通注册】:传入 Type 对象
 builder.Services.AddSingleton(typeof(ILoggerService), typeof(ConsoleLogger));
 builder.Services.AddScoped(typeof(IUserService), typeof(UserService));
 ​
 var app = builder.Build();
 ​
 app.MapPost("/users", (string name, IUserService userService) =>
 {
     userService.CreateUser(name);
     return Results.Ok();
 });
 ​
 app.Run();

你可能会问:“这不就是把尖括号换成了 typeof 吗?为什么要搞这么麻烦?”

2. 核心价值:解决“运行时动态注册”和“开放泛型”

普通注册的真正威力,在于在编写代码时不知道具体类型,需要在运行时通过反射动态获取的场景。

场景 A:开放泛型注册(Open Generics)

假设你有一个通用的仓储接口 IRepository<T> 和实现 Repository<T>

C#

代码语言:javascript
复制
 // 开放泛型,T 是不确定的
 public interface IRepository<T> { ... }
 public class Repository<T> : IRepository<T> { ... }

你无法用泛型注册写出 AddScoped<IRepository<T>, Repository<T>>(),因为这里的 T 是未知的!

此时必须用普通注册:

C#

代码语言:javascript
复制
 // 开放泛型注册:用 typeof(IRepository<>) 占位
 builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
场景 B:程序集扫描(Assembly Scanning)

当你的项目有几百个 Service 时,手动一个个写 AddScoped 会累死。你可以用反射扫描程序集,批量注册:

C#

代码语言:javascript
复制
 var assembly = typeof(Program).Assembly;
 ​
 // 获取所有以 "Service" 结尾的类,并用普通注册自动注入
 var serviceTypes = assembly.GetTypes()
     .Where(t => t.Name.EndsWith("Service") && !t.IsInterface);
 ​
 foreach (var implType in serviceTypes)
 {
     var interfaceType = implType.GetInterfaces().FirstOrDefault();
     if (interfaceType != null)
     {
         // 只能使用普通注册,因为 implType 和 interfaceType 是运行时算出来的 Type 变量!
         builder.Services.AddScoped(interfaceType, implType);
     }
 }

四、 第三种:工厂函数注册(Func<IServiceProvider, T>)

“当你需要手控创建逻辑、或者传入动态参数时使用。”

前两种注册方式(泛型与普通)都有一个共同前提:创建对象的过程完全交给容器去反射 new

但如果对象的创建非常复杂(比如要读配置文件、传动态参数、做条件分支决策),容器自己“猜”不出来怎么 new 时,就需要用到工厂函数注册

1. 怎么写?

工厂函数注册接收一个 Lambda 表达式 sp => ...

  • sp:代表 IServiceProvider(服务提供者)。你可以通过它在函数内部解析其他已经注册过的服务
  • 返回值:你亲手 new 出来的具体对象。
示例:根据环境选择不同的实现,并传入自定义配置

假设我们的 ConsoleLogger 构造函数需要传入一个前缀字符串:

C#

代码语言:javascript
复制
 public class CustomLogger : ILoggerService
 {
     private readonly string _prefix;
     public CustomLogger(string prefix) => _prefix = prefix;
 ​
     public void Log(string message) => Console.WriteLine($"[{_prefix}]: {message}");
 }

在 Minimal API 中用工厂函数注册:

C#

代码语言:javascript
复制
 var builder = WebApplication.CreateBuilder(args);
 ​
 // 【工厂函数注册】:我自己写代码来 new 这个对象!
 builder.Services.AddSingleton<ILoggerService>(sp =>
 {
     // 1. 可以通过 sp 拿到配置服务
     var config = sp.GetRequiredService<IConfiguration>();
     var appName = config["AppName"] ?? "DefaultApp";
 ​
     // 2. 亲自 new 出 CustomLogger 并传参
     return new CustomLogger(prefix: appName);
 });
 ​
 // 如果 UserService 需要特殊处理,也可以用工厂函数:
 builder.Services.AddScoped<IUserService>(sp =>
 {
     // 从容器中解析出 Logger,再手工构造 UserService
     var logger = sp.GetRequiredService<ILoggerService>();
     return new UserService(logger);
 });
 ​
 var app = builder.Build();
 ​
 app.MapGet("/", (ILoggerService logger) =>
 {
     logger.Log("Hello Factory!");
     return Results.Ok();
 });
 ​
 app.Run();

2. 适用场景总结

  1. 构造函数需要传入非 DI 参数:如连接字符串、API Key、普通基元类型 (string, int)。
  2. 存在条件分支决策:比如 if (isDev) return new FakePayment(); else return new RealPayment();
  3. 性能与 Native AOT 极致优化:直接用手写 C# 代码 new,零反射,在 Native AOT 发布时 100% 安全且性能最高。

五、 三种注册方式全方位对比表

注册方式

典型代码示例

适合什么场景?

优点

缺点

泛型注册

AddScoped<IService, Service>()

90% 的日常业务开发。

简单直观、类型安全、Native AOT 友好。

无法处理未知的动态类型,无法传入自定义非 DI 参数。

普通注册

AddScoped(typeof(IService), typeof(Service))

开放泛型 (IRepository<>)、程序集反射批量注册。

极为灵活,支持在运行时动态计算类型进行注册。

无法进行编译期类型检查,写法稍显繁琐。

工厂函数注册

AddScoped<IService>(sp => new Service(...))

复杂对象构造、需要传入配置/动态参数、条件分支。

控制力极强,完全由你决定如何实例化对象;对 Native AOT 极度友好。

逻辑复杂时代码量增加,如果过度使用会退化为手动构造。

六、 总结与最佳实践

在写 Minimal API 时,你可以遵循以下“无脑选择指南”:

  1. 优先选【泛型注册】:只要接口和实现类都是明确的,直接用 builder.Services.AddScoped<IOrderService, OrderService>()
  2. 遇到框架封装/开放泛型选【普通注册】:比如注册全局仓储 IRepository<>,或者用反射批量注入层级代码时。
  3. 遇到“传不进去参数”或“需要判断条件”选【工厂函数注册】:不要硬拧构造函数,用 sp => new MyService(dynamicArg) 轻松解决。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 彻底搞懂 .NET DI 的三种注册姿势:普通、泛型与工厂函数
    • 一、 预备知识:我们要解决什么问题?
    • 二、 第一种:泛型注册(最常用、最优雅)
      • 1. 怎么写?
      • 2. 特点与适用场景
    • 三、 第二种:普通注册(基于 Type 的非泛型注册)
      • 1. 怎么写?
      • 2. 核心价值:解决“运行时动态注册”和“开放泛型”
    • 四、 第三种:工厂函数注册(Func<IServiceProvider, T>)
      • 1. 怎么写?
      • 2. 适用场景总结
    • 五、 三种注册方式全方位对比表
    • 六、 总结与最佳实践
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档