keyed service要求注册、解析、注入三者严格对齐:必须用addkeyedxxx注册,构造函数注入需加[fromkeyedservices]特性,手动解析应使用getrequiredkeyedservice,键类型需完全一致(含大小写、枚举值等)。

Keyed Service 不是“多注册一个接口就能用”的功能,它要求注册、解析、注入三者严格对齐,漏掉任意一环都会导致 InvalidOperationException 或注入为 null。
注册时必须用 AddKeyedXXX 方法,不能混用普通注册
如果你注册了两个实现但没用键控方法,比如:
builder.Services.AddScoped<iemailservice smtpemailservice>(); builder.Services.AddScoped<iemailservice sendgridemailservice>();</iemailservice></iemailservice>
这只会让容器保留最后一个注册项,且无法通过键获取。正确做法是显式使用键控注册:
AddKeyedScoped<iemailservice smtpemailservice>("smtp")</iemailservice>AddKeyedScoped<iemailservice sendgridemailservice>("sendgrid")</iemailservice>- 键类型不限于
string,也支持enum、int等可比较类型(如ServiceInstanceEnum.Smtp) - 不支持
null作为键;若需“默认兜底”,得手动注册一个带特定键(如"default")的实现
构造函数注入必须加 [FromKeyedServices] 特性
即使注册了键控服务,直接写 IEmailService service 参数仍会失败——容器不知道你要哪个键。必须显式标注:
public class OrderService
{
private readonly IEmailService _emailService;
public OrderService([FromKeyedServices("smtp")] IEmailService emailService)
{
_emailService = emailService;
}
}
常见错误:
- 忘了加特性,报错:
Unable to resolve service for type 'IEmailService' - 特性里键名拼错(如
"smpt"),运行时报InvalidOperationException: No service for type ... with key 'smpt' - 在非 DI 创建的类中使用(如
new OrderService(...)),字段始终为null
手动解析要用 GetRequiredKeyedService 而不是 GetService
在 BackgroundService、Timer 回调等非构造函数场景,不能依赖自动注入,必须手动从作用域内取:
using var scope = _serviceProvider.CreateScope();
var smtpService = scope.ServiceProvider.GetRequiredKeyedService<iemailservice>("smtp");</iemailservice>
注意区别:
-
GetService<iemailservice>()</iemailservice>→ 返回null(不识别键) -
GetKeyedService<iemailservice>("smtp")</iemailservice>→ 可能返回null(键不存在时不抛异常) -
GetRequiredKeyedService<iemailservice>("smtp")</iemailservice>→ 键不存在时立即抛异常,推荐用于关键路径 - 必须在有效作用域内调用,不能直接用根
IServiceProvider(否则可能拿到单例或报错)
别名键和枚举键的兼容性差异
字符串键最常用,但枚举键在大型项目中更安全(编译期校验):
public enum EmailChannel { Smtp, SendGrid }
// 注册
builder.Services.AddKeyedScoped<iemailservice smtpemailservice>(EmailChannel.Smtp);
// 注入
public Controller([FromKeyedServices(EmailChannel.Smtp)] IEmailService svc) { ... }</iemailservice>
但要注意:
- 枚举值不能是
0且未定义(如enum E { A=0, B }中A可用,但default(E)会变成0,可能意外命中) - JSON 序列化/反序列化时,枚举键不会自动转成字符串,调试日志里看到的是数字,容易误判
- 跨程序集共享枚举键时,确保所有项目引用同一份定义,否则键不匹配
键控服务的核心约束就一条:键必须完全一致——注册用什么类型和值,解析时就得用一模一样的东西。大小写、空格、枚举底层值,差一点就找不到。











