
在 Modern .NET(尤其是 Minimal API)开发中,依赖注入(DI)是我们每天都要打交道的工具。
在 Program.cs 里,你肯定经常写 builder.Services.AddScoped(...)。但你可能会发现,这个 API 提供了好几种不同的写姿势:有时候传两个类型,有时候传一个泛型,有时候里面还带个 sp => 的 Lambda 表达式。
这些注册方式到底有什么区别?分别适用于什么场景?
本文将基于 .NET Minimal API,用最通俗易懂的方式为你拆解 DI 的三种核心注册姿势:普通注册、泛型注册 和 工厂函数注册。
假设我们正在开发一个用户管理系统。
我们需要一个日志服务 ILoggerService 及其实现类 ConsoleLogger,以及一个处理用户业务的 UserService。
C#
// 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 在运行时能够自动拿到它们。
“最推荐的日常写法,类型安全,代码干净。”
在 Minimal API 中,泛型注册是我们 90% 的场景下都在写的形式。使用尖括号 <Service, Implementation> 明确指定接口类型和实现类型。
C#
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();“适合反射、批量注册和框架级封装。”
普通注册不使用尖括号 <>,而是直接通过 C# 的 typeof(...) 传入 Type 类型的参数。
C#
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 吗?为什么要搞这么麻烦?”
普通注册的真正威力,在于在编写代码时不知道具体类型,需要在运行时通过反射动态获取的场景。
假设你有一个通用的仓储接口 IRepository<T> 和实现 Repository<T>:
C#
// 开放泛型,T 是不确定的
public interface IRepository<T> { ... }
public class Repository<T> : IRepository<T> { ... }你无法用泛型注册写出 AddScoped<IRepository<T>, Repository<T>>(),因为这里的 T 是未知的!
此时必须用普通注册:
C#
// 开放泛型注册:用 typeof(IRepository<>) 占位
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));当你的项目有几百个 Service 时,手动一个个写 AddScoped 会累死。你可以用反射扫描程序集,批量注册:
C#
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);
}
}“当你需要手控创建逻辑、或者传入动态参数时使用。”
前两种注册方式(泛型与普通)都有一个共同前提:创建对象的过程完全交给容器去反射 new。
但如果对象的创建非常复杂(比如要读配置文件、传动态参数、做条件分支决策),容器自己“猜”不出来怎么 new 时,就需要用到工厂函数注册。
工厂函数注册接收一个 Lambda 表达式 sp => ...。
sp:代表 IServiceProvider(服务提供者)。你可以通过它在函数内部解析其他已经注册过的服务。new 出来的具体对象。假设我们的 ConsoleLogger 构造函数需要传入一个前缀字符串:
C#
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#
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();string, int)。if (isDev) return new FakePayment(); else return new RealPayment();。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 时,你可以遵循以下“无脑选择指南”:
builder.Services.AddScoped<IOrderService, OrderService>()。IRepository<>,或者用反射批量注入层级代码时。sp => new MyService(dynamicArg) 轻松解决。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。