georgepwall1991/DependencyInjection.Lifetime.Analyzers
GitHub: georgepwall1991/DependencyInjection.Lifetime.Analyzers
一款基于 Roslyn 的 .NET 依赖注入生命周期编译期分析器,用于在构建阶段捕获囚禁依赖、未释放作用域、循环依赖和注册错误等问题。
Stars: 9 | Forks: 0
# DependencyInjection.Lifetime.Analyzers
**一个用于 C# 和 ASP.NET Core 生命周期与激活 Bug 的 Roslyn 依赖注入分析器包。**
在编译期间捕获 DI 范围泄漏、囚禁依赖、`BuildServiceProvider()` 误用、循环依赖以及无法解析或无法实例化的服务,且无任何运行时开销。
[](https://www.nuget.org/packages/DependencyInjection.Lifetime.Analyzers)
[](https://www.nuget.org/packages/DependencyInjection.Lifetime.Analyzers)
[](https://opensource.org/licenses/MIT)
[](https://github.com/georgepwall1991/DependencyInjection.Lifetime.Analyzers/actions/workflows/ci.yml)
[](https://github.com/georgepwall1991/DependencyInjection.Lifetime.Analyzers/actions/workflows/ci.yml)
[可搜索的文档站点](https://georgepwall1991.github.io/DependencyInjection.Lifetime.Analyzers/) · [规则索引](https://georgepwall1991.github.io/DependencyInjection.Lifetime.Analyzers/rules/) · [问题指南](https://georgepwall1991.github.io/DependencyInjection.Lifetime.Analyzers/problems/) · [NuGet 包](https://www.nuget.org/packages/DependencyInjection.Lifetime.Analyzers)
`DependencyInjection.Lifetime.Analyzers` 适用于使用 `Microsoft.Extensions.DependencyInjection` 的团队,他们希望在编译期获得保护,以防止那些通常在运行时表现为 Bug、不稳定测试或仅在 生产环境 出现的启动失败的 DI 生命周期错误。
- 适用于 Rider、Visual Studio 以及 `dotnet build` / CI 环境。
- 覆盖 ASP.NET Core、worker services、控制台应用以及通过默认 DI 容器连接服务的库代码。
- 提供 19 个针对性诊断,并在安全且明确的情况下提供代码修复。
## 为什么需要这个 DI 生命周期分析器
`DependencyInjection.Lifetime.Analyzers` 可帮助使用 `Microsoft.Extensions.DependencyInjection` 的团队避免常见的生产环境问题:
- 无效范围使用导致的 `ObjectDisposedException`。
- 未释放的范围或根提供程序导致的内存泄漏。
- 不正确的服务生命周期导致的囚禁依赖 Bug。
- 削弱可测试性的隐藏 服务定位器 用法。
- 缺少或不兼容的注册导致的运行时激活失败。
此分析器包专为需要可靠依赖注入规则的 **ASP.NET Core**、**worker services**、**控制台应用**和 **CI 流水线**而设计。
## 为什么团队要安装它
- 在囚禁依赖变成陈旧状态或线程安全 Bug 之前发现它们。
- 在范围泄漏变成 `ObjectDisposedException` 事件或内存泄漏之前捕获它们。
- 在启动或后台作业激活在生产环境失败之前,检测缺失的注册和实现不匹配。
- 在循环依赖链和不可实例化的注册在运行时失败之前捕获它们。
- 将 DI 架构规则引入 CI,而不是依赖代码审查的记忆。
## 快速入门
从 NuGet 安装:
```
dotnet add package DependencyInjection.Lifetime.Analyzers --version 2.8.26
```
或直接添加包引用:
```
all
```
对于中央包管理 (`Directory.Packages.props`):
```
```
然后从项目文件中引用它:
```
```
在 `.editorconfig` 中设置有用的严重性:
```
[*.cs]
dotnet_diagnostic.DI003.severity = error
dotnet_diagnostic.DI013.severity = error
dotnet_diagnostic.DI007.severity = suggestion
dotnet_diagnostic.DI011.severity = suggestion
```
默认情况下,运行时失败和面向泄漏的规则保持在 `Warning` 或 `Error` 级别,而更广泛的设计气味规则(如 `DI007`、`DI010`、`DI011` 和 `DI012`)默认为 `Info` 级别,以减少嘈杂的诊断。
有关推广检查清单和入门严重性策略,请参阅 [docs/ADOPTION.md](docs/ADOPTION.md)。
## 适用人群
- 在 ASP.NET Core 或通用主机应用中使用 `Microsoft.Extensions.DependencyInjection` 的团队。
- 希望在 CI 中(而不仅是在运行时)保护 DI 用法的库和内部平台。
- 使用工厂、键控服务、`ActivatorUtilities` 或手动范围的代码库。
- 试图随着时间的推移减少由 `IServiceProvider` 驱动的 服务定位器 蔓延的维护者。
## 它能捕获什么
| 问题领域 | 示例规则 |
|--------------|---------------|
| 范围释放与泄漏 | `DI001`, `DI004`, `DI005`, `DI014` |
| 生命周期不匹配 | `DI003`, `DI009` |
| 服务定位与架构偏移 | `DI006`, `DI007`, `DI011` |
| 注册正确性与激活有效性 | `DI012`, `DI013`, `DI015`, `DI016`, `DI018` |
| 依赖图正确性 | `DI017` |
| 根提供程序生命周期验证 | `DI019` |
| 构造函数与组合气味检测 | `DI010` |
## 目录
- [为什么需要这个 DI 生命周期分析器](#why-this-di-lifetime-analyser)
- [为什么团队要安装它](#why-teams-install-it)
- [快速入门](#quickstart)
- [适用人群](#who-this-is-for)
- [它能捕获什么](#what-it-catches)
- [规则索引](#rule-index)
- [DI001: 服务范围未释放](#di001-service-scope-not-disposed)
- [DI002: 范围服务逃逸范围](#di002-scoped-service-escapes-scope)
- [DI003: 囚禁依赖](#di003-captive-dependency)
- [DI004: 范围释放后使用服务](#di004-service-used-after-scope-disposed)
- [DI005: 在异步方法中使用 `CreateAsyncScope`](#di005-use-createasyncscope-in-async-methods)
- [DI006: 静态 `IServiceProvider` 缓存](#di006-static-iserviceprovider-cache)
- [DI007: 服务定位器反模式](#di007-service-locator-anti-pattern)
- [DI008: 可释放的瞬时服务](#di008-disposable-transient-service)
- [DI009: 开放泛型囚禁依赖](#di009-open-generic-captive-dependency)
- [DI010: 构造函数过度注入](#di010-constructor-over-injection)
- [DI011: `IServiceProvider` 注入](#di011-iserviceprovider-injection)
- [DI012: 条件注册误用](#di012-conditional-registration-misuse)
- [DI013: 实现类型不匹配](#di013-implementation-type-mismatch)
- [DI014: 根服务提供程序未释放](#di014-root-service-provider-not-disposed)
- [DI015: 无法解析的依赖](#di015-unresolvable-dependency)
- [DI016: BuildServiceProvider 误用](#di016-buildserviceprovider-misuse)
- [DI017: 循环依赖](#di017-circular-dependency)
- [DI018: 不可实例化的实现类型](#di018-non-instantiable-implementation-type)
- [DI019: 从根提供程序解析范围服务](#di019-scoped-service-resolved-from-root-provider)
- [配置](#configuration)
- [采用指南](#adoption-guide)
- [常见问题](#frequently-asked-questions)
## 规则索引
| ID | 标题 | 默认严重性 | 代码修复 |
|----|-------|------------------|----------|
| [DI001](#di001-service-scope-not-disposed) | 服务范围未释放 | Warning | 是 |
| [DI002](#di002-scoped-service-escapes-scope) | 范围服务逃逸范围 | Warning | 是 |
| [DI003](#di003-captive-dependency) | 囚禁依赖 | Warning | 是 |
| [DI004](#di004-service-used-after-scope-disposed) | 范围释放后使用服务 | Warning | 是 |
| [DI005](#di005-use-createasyncscope-in-async-methods) | 在异步方法中使用 `CreateAsyncScope` | Warning | 是 |
| [DI006](#di006-static-iserviceprovider-cache) | 静态 `IServiceProvider` 缓存 | Warning | 是 |
| [DI007](#di007-service-locator-anti-pattern) | 服务定位器反模式 | Info | 否 |
| [DI008](#di008-disposable-transient-service) | 可释放的瞬时服务 | Warning | 是 |
| [DI009](#di009-open-generic-captive-dependency) | 开放泛型囚禁依赖 | Warning | 是 |
| [DI010](#di010-constructor-over-injection) | 构造函数过度注入 | Info | 否 |
| [DI011](#di011-iserviceprovider-injection) | `IServiceProvider` 注入 | Info | 否 |
| [DI012](#di012-conditional-registration-misuse) | 条件/重复注册误用 | Info | 是 |
| [DI013](#di013-implementation-type-mismatch) | 实现类型不匹配 | Error | 是 |
| [DI014](#di014-root-service-provider-not-disposed) | 根提供程序未释放 | Warning | 是 |
| [DI015](#di015-unresolvable-dependency) | 无法解析的依赖 | Warning | 是 |
| [DI016](#di016-buildserviceprovider-misuse) | 注册期间 BuildServiceProvider 误用 | Warning | 否 |
| [DI017](#di017-circular-dependency) | 循环依赖 | Warning | 否 |
| [DI018](#di018-non-instantiable-implementation-type) | 不可实例化的实现类型 | Warning | 否 |
| [DI019](#di019-scoped-service-resolved-from-root-provider) | 从根提供程序解析范围服务 | Warning | 否 |
## DI001: 服务范围未释放
**捕获内容:** 使用 `CreateScope()` 或 `CreateAsyncScope()` 创建的 `IServiceScope` 实例从未被释放,包括唯一的释放调用被隐藏在条件分支、switch 部分、循环、catch 块中,或者在可能绕过共享清理的分支退出之后。DI001 能识别条件赋值的预声明可空范围局部变量,当随后的条件访问、非空守卫、同分支预退出或 `finally` 释放能可靠地结束所有权时,同时仍会报告重新赋值泄漏和需要每次迭代释放的循环创建的范围。
**重要性:** 未释放的范围可能比预期更长时间地保留范围和瞬时可释放服务,从而导致内存和句柄泄漏。
**问题:**
```
public void Process()
{
var scope = _scopeFactory.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService
();
svc.Run();
}
```
**更好的模式:**
```
public void Process()
{
using var scope = _scopeFactory.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService();
svc.Run();
}
```
**代码修复:** 是。在可能的情况下添加 `using` / `await using`。
## DI002: 范围服务逃逸范围
**捕获内容:** 从范围中解析的服务被返回或存储在生命周期更长的地方,包括通过提供程序别名解析的服务、捕获范围服务然后逃逸的委托、稍后通过 `using (scope)` 释放的范围,以及构造函数、访问器、局部函数、lambda 和匿名方法中的相同模式。
**重要性:** 一旦范围被释放,该服务可能指向已释放的状态。
**问题:**
```
public IMyService GetService()
{
using var scope = _scopeFactory.CreateScope();
return scope.ServiceProvider.GetRequiredService();
}
```
**更好的模式:**
```
public void UseServiceNow()
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService();
service.Execute();
}
```
**代码修复:** 是(对于直接重构不切实际的故意接受的情况,提供抑制选项)。
## DI003: 囚禁依赖
**捕获内容:** 单例服务捕获了范围或瞬时依赖,包括构造函数注入、`IEnumerable` 集合捕获、已知的范围框架服务(如 `IOptionsSnapshot`)、EF Core 上下文和来自 `AddDbContext(...)`、`AddDbContextFactory(...)`、`AddDbContextPool(...)` 和 `AddPooledDbContextFactory(...)` 的 `DbContextOptions` 注册,包括服务/实现重载自注册,以及高置信度工厂路径(如内联委托、稳定的局部委托工厂、方法组工厂、`GetServices()`、键控解析以及没有显式构造函数参数的 `ActivatorUtilities.CreateInstance(...)`)。
**重要性:** 生命周期不匹配会产生陈旧状态、泄漏和线程安全缺陷。
**问题:**
```
services.AddScoped();
services.AddSingleton();
public sealed class SingletonService : ISingletonService
{
public SingletonService(IScopedService scoped) { }
}
```
**更好的模式:**
```
services.AddScoped();
// or keep singleton and create scopes inside operations
public sealed class SingletonService : ISingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public SingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public void Run()
{
using var scope = _scopeFactory.CreateScope();
var scoped = scope.ServiceProvider.GetRequiredService();
scoped.DoWork();
}
}
```
**由 DbContext 支持的处理器:**
```
services.AddDbContext();
services.AddScoped();
services.AddHostedService();
public sealed class ProcessorHostedService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public ProcessorHostedService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService();
await processor.RunAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken) =>
Task.CompletedTask;
}
```
当 Repository 和 unit-of-work 抽象的注册生命周期为范围或瞬时时,将会被报告。DI003 不会仅从 `IRepository` 或 `IUnitOfWork` 这样的名称推断由 DbContext 支持的行为。
**代码修复:** 是。当注册语法是局部且明确时(例如 `AddSingleton`、`TryAddSingleton`、带键的 `AddKeyedSingleton`、内联工厂注册以及受支持的 `ServiceDescriptor` 形式),重写显式的注册生命周期。
## DI004: 范围释放后使用服务
**捕获内容:** 在生成服务的范围结束后使用该服务,包括通过提供程序别名解析的服务、在释放后枚举的来自 `GetServices()` 的范围集合、显式的 `Dispose()` / `DisposeAsync()`、稍后通过 `using (scope)` 释放的范围,以及构造函数、访问器、局部函数、lambda 和匿名方法中的相同模式。
**重要性:** 会导致运行时释放错误和脆弱的服务行为。
**问题:**
```
IMyService service;
using (var scope = _scopeFactory.CreateScope())
{
service = scope.ServiceProvider.GetRequiredService();
}
service.DoWork();
```
**更好的模式:**
```
using (var scope = _scopeFactory.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService();
service.DoWork();
}
```
**代码修复:** 是。仅当诊断局部变量是在该范围中赋值的时,才将简单的立即调用式用法移回拥有范围中,或者为依赖上下文的情况添加窄杂注抑制。
## DI005: 在异步方法中使用 `CreateAsyncScope`
**捕获内容:** 在需要异步释放的异步流中使用了 `CreateScope()`,包括异步方法、lambda、局部函数、匿名方法以及使用 `await 的顶级程序。检测覆盖了常规成员访问(`_scopeFactory.CreateScope()`)和条件访问接收者(`_scopeFactory?.CreateScope()`、`_provider?.CreateScope()`)。
**重要性:** 异步可释放对象(`IAsyncDisposable`)可能无法通过同步释放模式正确清理。
**问题:**
```
public async Task RunAsync()
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService();
await service.ExecuteAsync();
}
```
**更好的模式:**
```
public async Task RunAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService();
await service.ExecuteAsync();
}
```
**代码修复:** 是。将安全的 `using` 范围创建/释放模式重写为 `await using` 加上 `CreateAsyncScope()`。
## DI006: 静态 `IServiceProvider` 缓存
**捕获内容:** 存储在静态字段或属性中的 `IServiceProvider` / `IServiceScopeFactory` / 键控提供程序,包括围绕这些提供程序类型的 `Lazy` 包装器。
**重要性:** 全局提供程序状态会助长服务定位器的使用并混淆生命周期边界。
**问题:**
```
public static class Locator
{
public static IServiceProvider Provider { get; set; } = null!;
private static readonly Lazy LazyProvider = new(() => Provider);
}
```
**更好的模式:**
```
public sealed class Locator
{
private readonly IServiceProvider _provider;
public Locator(IServiceProvider provider)
{
_provider = provider;
}
}
```
**代码修复:** 是。在现有引用保持有效的常见私有成员情况下移除 `static` 修饰符。
## DI007: 服务定位器反模式
**捕获内容:** 在应用逻辑中通过 `IServiceProvider` 解析依赖。
**重要性:** 隐藏了真实的依赖,增加了测试难度,并削弱了架构边界。
**问题:**
```
public sealed class MyService
{
private readonly IServiceProvider _provider;
public MyService(IServiceProvider provider)
{
_provider = provider;
}
public void Run()
{
var dep = _provider.GetRequiredService();
dep.Execute();
}
}
```
**更好的模式:**
```
public sealed class MyService
{
private readonly IDependency _dependency;
public MyService(IDependency dependency)
{
_dependency = dependency;
}
public void Run() => _dependency.Execute();
}
```
**代码修复:** 否。这通常属于架构重构。
DI007 在已知的组合/工厂边界中保持静默:DI 注册工厂、返回值的 `Create*`/`Build*` 工厂方法、第一个参数为 `HttpContext` 的 ASP.NET Core 中间件 `Invoke`/`InvokeAsync` 方法、`BackgroundService.ExecuteAsync`、精确的托管服务生命周期实现、选项配置/验证实现,以及感知提供程序的选项/工厂委托。
## DI008: 可释放的瞬时服务
**捕获内容:** 以高风险模式实现 `IDisposable`/`IAsyncDisposable` 的瞬时服务。
**重要性:** 释放所有权可能变得不明确,并且资源可能会泄漏。
**问题:**
```
services.AddTransient();
public sealed class DisposableService : IMyService, IDisposable
{
public void Dispose() { }
}
```
**更好的模式:**
```
services.AddScoped();
// or ensure explicit disposal ownership if transient is intentional
```
DI008 遵循泛型、`typeof(...)`、键控、命名参数、`ServiceDescriptor.Transient(...)`、`ServiceDescriptor.Describe(..., ServiceLifetime.Transient)`、`new ServiceDescriptor(..., ServiceLifetime.Transient)`、`TryAddTransient` 和 `TryAddEnumerable` 注册形式。工厂注册保持静默,因为在用户代码中释放所有权是明确的。
**代码修复:** 是。建议更安全的生命周期替代方案,并在注册明确的地方重写局部描述符生命周期参数。
## DI009: 开放泛型囚禁依赖
**捕获内容:** 依赖于生命周期更短服务的开放泛型单例注册,包括 `TryAddSingleton(...)`、`ServiceDescriptor.Singleton(...)`、键控开放泛型单例注册,以及元素服务生命周期较短的 `IEnumerable` 构造函数捕获。
**重要性:** 每个封闭泛型实例都会继承生命周期不匹配的问题。
**问题:**
```
services.AddScoped();
services.AddSingleton(typeof(IRepository<>), typeof(Repository<>));
public sealed class Repository : IRepository
{
public Repository(IScopedService scoped) { }
}
```
**更好的模式:**
```
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
```
DI009 遵循容器实际可以使用的单一可能激活构造函数。可选/默认值参数在该选择期间被视为可激活的,而模糊的同等贪婪构造函数集则保持静默,而不是去猜测。
依赖生命周期首先根据用户注册进行查找,然后回退到共享的已知框架分类器,因此捕获 `IOptionsSnapshot` 的开放泛型单例将被报告为范围捕获,即使应用程序没有手动注册 Options。`IOptions` 和 `IOptionsMonitor` 保留其单例生命周期并保持静默。
**代码修复:** 是。可以调整开放泛型注册的生命周期。
## DI010: 构造函数过度注入
**捕获内容:** 具有过多有意义依赖的构造函数。
**重要性:** 通常表明一个类承担了过多的职责。
**问题:**
```
public sealed class ReportingService
{
public ReportingService(
IDep1 dep1,
IDep2 dep2,
IDep3 dep3,
IDep4 dep4,
IDep5 dep5)
{
}
}
```
**更好的模式:** 拆分为专注的协作者并注入更小的抽象。
对于常规类型注册,DI010 会评估容器实际可以激活的公共构造函数,而不是每个声明的构造函数。它还涵盖直接返回 `new MyService(...)` 的简单工厂注册、在 `return new MyService(...)` 之前设置局部变量的最终返回工厂块以及 `ActivatorUtilities.CreateInstance(sp)`,同时对分支或动态工厂保持保守。
默认情况下,当一个构造函数具有超过 `4` 个有意义的依赖时,DI010 会进行报告。它忽略基元/值类型、可选参数、已被 `DI011` 覆盖的提供程序管道类型以及常见的框架抽象,如 `ILogger`、`IOptions` 和 `IConfiguration`。
在 `.editorconfig` 中配置阈值:
```
[*.cs]
dotnet_code_quality.DI010.max_dependencies = 5
```
**代码修复:** 否。需要设计决策。
## DI011: `IServiceProvider` 注入
**捕获内容:** 在常规服务中构造函数注入 `IServiceProvider`、`IServiceScopeFactory` 或 `IKeyedServiceProvider`。
**重要性:** 这通常会启用隐藏的运行时解析和服务定位器行为。
**问题:**
```
public sealed class MyService
{
public MyService(IServiceProvider provider) { }
}
```
**更好的模式:** 直接注入具体的依赖。
**代码修复:** 否。将提供程序管道替换为显式依赖是一项设计决策。
**此规则中的已知例外:** 具有返回值的工厂成员的工厂式类型、故意使用 `IServiceScopeFactory` 创建范围的单例服务、第一个参数为 `HttpContext` 的 ASP.NET Core 中间件 `Invoke`/`InvokeAsync` 方法、托管服务、端点筛选器工厂,以及容器无法激活的非公共构造函数上的提供程序参数。
## DI012: 条件注册误用
**捕获内容:**
- 在 `Add*` 已经注册了该服务之后的 `TryAdd*` 调用。
- 后续条目覆盖早期条目的重复 `Add*` 注册。
DI012 也遵循跨局部别名和源定义的辅助/局部函数包装器的相同 `IServiceCollection` 流,同时对不透明的辅助边界保持保守,而不是去猜测注册顺序。
**重要性:** 注册意图变得不明确,并且行为与读者预期的不同。
**问题:**
```
services.AddSingleton();
services.TryAddSingleton(); // ignored
services.AddSingleton();
services.AddSingleton(); // overrides A
```
**更好的模式:** 决定并清楚地表明意图:先使用 `TryAdd*`,或者使用带有注释/测试的显式覆盖。
**代码修复:** 对于作为块包含的独立语句的被忽略 `TryAdd*` 和 `TryAddKeyed*` 调用,支持修复;修复器会删除多余的忽略注册。重复覆盖的情况和嵌入的单行语句主体仍需手动处理。
## DI013: 实现类型不匹配
**捕获内容:** 可以编译但在运行时失败的无效服务/实现对,包括泛型、`typeof(...)`、键控、命名参数和 `ServiceDescriptor` 注册。
**重要性:** 服务激活在运行时抛出异常(根据路径不同,抛出 `ArgumentException`/`InvalidOperationException`)。
**问题:**
```
public interface IRepository { }
public sealed class WrongType { }
services.AddSingleton(typeof(IRepository), typeof(WrongType));
```
**更好的模式:**
```
public sealed class SqlRepository : IRepository { }
services.AddSingleton(typeof(IRepository), typeof(SqlRepository));
```
**代码修复:** 是。在语法和符号足够局部以便安全重写的情况下提供广泛协助:删除无效的块包含独立注册,用兼容的候选替换实现类型,或者将服务类型重新定位为由当前实现实现的接口/基类型,包括无效的实现实例注册。除非有符号支持的类型重写可用,否则嵌入的单行语句主体仍需手动处理。
## DI014: 根服务提供程序未释放
**捕获内容:** 来自 `BuildServiceProvider()` 的根提供程序从未被释放,包括唯一的手动释放是条件性的、仅在 catch 中、在重新赋值给另一个提供程序之后,或者在循环内重复创建之后的局部提供程序。
**重要性:** 根范围内的单例可释放对象可能永远不会被清理。
**问题:**
```
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService();
```
**更好的模式:**
```
using var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService();
```
**代码修复:** 是。为没有现有手动释放代码的简单局部声明添加释放模式。条件性或部分手动释放流仅保留诊断,以便所有权重写仍然是深思熟虑的。
## DI015: 无法解析的依赖
**捕获内容:** 其直接或传递构造函数/工厂依赖未注册的已注册服务(包括键控和开放泛型路径)。
**重要性:** 当 DI 尝试创建服务时,运行时激活会失败。
**问题:**
```
public interface IMissingDependency { }
public interface IMyService { }
public sealed class MyService : IMyService
{
public MyService(IMissingDependency missing) { }
}
services.AddSingleton();
```
**更好的模式:**
```
public sealed class MissingDependency : IMissingDependency { }
services.AddScoped();
services.AddSingleton();
```
**代码修复:** 是。当 DI015 可以证明单个直接具体依赖可以安全注册时,添加缺失的自绑定注册。支持局部构造函数诊断、`TryAdd*` 注册站点、局部 `IServiceCollection` 别名、直接的 `GetRequiredService()` 工厂诊断,以及当键可以作为 C# 字面量发出时的键控自绑定。
### DI015 严格模式
默认情况下,DI015 假定宿主提供的常见框架服务(日志/选项/配置)可用。通过 `AddDbContext(...)`、`AddDbContextFactory(...)`、`AddDbContextPool(...)` 或 `AddPooledDbContextFactory(...)` 注册的 EF Core 上下文也被建模为注册,包括服务/实现重载自注册以及这些模式所需的 `DbContextOptions` 和 `IDbContextFactory` 依赖。
禁用该假设以进行更严格的分析:
```
[*.cs]
dotnet_code_quality.DI015.assume_framework_services_registered = false
```
DI015 刻意保持保守以将误报保持在较低水平:
- 源可见的 `IServiceCollection` 包装器在 DI015 报告缺失注册之前会被展开。
- 稳定的局部委托工厂会被检查,包括继承的键控工厂参数、稍后的确定简单重新赋值、详尽的局部函数分支重写,以及对重写工厂的局部函数的方法组委托别名,而不相关的赋值左侧用法和不透明的委托局部写入(如直接委托调用、委托 `.Invoke()` 调用和 `ref`/`out` 写入)则保持保守。
- `[ServiceKey]` 参数和 `IEnumerable` 被视为由容器提供。
- 无参数的 `[FromKeyedServices]` 在包含键已知时继承包含的键控注册键。
- `KeyedService.AnyKey` 键控注册满足精确的键控依赖请求。
- 同流的确定 `RemoveAll(...)` 和 `Replace(...)` 变异会抑制它们移除的注册的诊断。
- 依赖循环被视为可解析的。
- 没有可检查依赖路径的工厂注册被视为可解析的。
- `GetService(...)` 和动态键控解析被视为可选/未知。
- 如果较早的不透明或外部包装器可能在相同的 `IServiceCollection` 流上注册了服务,DI015 会保持静默,而不是进行推测。
- 如果任何有效的候选注册由不透明的工厂支持,DI015 会保持静默,而不是进行推测。
## DI016: BuildServiceProvider 误用
**捕获内容:** 在组合注册期间(例如在 `ConfigureServices`、`IServiceCollection` 扩展注册方法、注册 lambda 或构建器风格的 `.Services` 辅助流中)的 `BuildServiceProvider()` 调用。
**重要性:** 在注册期间构建第二个提供程序可能会复制单例实例并产生生命周期不一致。
**问题:**
```
public static IServiceCollection AddFeature(this IServiceCollection services)
{
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService();
return services;
}
```
**更好的模式:**
```
public static IServiceCollection AddFeature(this IServiceCollection services, IMyOptions options)
{
// Use provided dependencies/options without creating a second container
return services;
}
```
**代码修复:** 否。
DI016 刻意保持保守以减少误报:
- 它仅报告注册上下文中经符号确认的 DI `BuildServiceProvider()` 调用。
- 它不报告有意返回 `IServiceProvider` 的提供程序工厂方法。
- 它识别可赋值的 `IServiceCollection` 抽象和来自 `.Services` 的同边界辅助/别名流,但不会对独立的顶级 `new ServiceCollection()` 组合根发出警告。
- 在调用点、辅助返回表达式或局部变量初始值设定项中,包装在 null 宽容运算符(`builder.Services!`)或同类型转换(`(IServiceCollection)builder.Services`)中的构建器 `.Services` 流仍被识别为注册上下文,而包装相同表达式的提供程序工厂方法则保持静默,因为它们返回 `IServiceProvider`。
- 条件访问调用(如 `builder.Services?.BuildServiceProvider()` 和 `builder?.Services.BuildServiceProvider()`)通过封闭的 `ConditionalAccessExpression` 和 `MemberBindingExpression` 形状的 `.Services` 访问被识别,因此空安全构建器流以与直接成员访问相同的方式参与检测。包装相同形状的提供程序工厂方法保持静默。
## DI017: 循环依赖
**捕获内容:** 构造函数注入循环,例如 `A -> B -> A`,包括更长的传递循环。当构造函数选择不明确时,它会保持静默,而不是去猜测容器会选择哪条路径。
**重要性:** 默认的 DI 容器无法解析循环的构造函数图,并且在激活服务时将在运行时失败。
**问题:**
```
services.AddScoped();
services.AddScoped();
public sealed class OrderService : IOrderService
{
public OrderService(IPaymentService payment) { }
}
public sealed class PaymentService : IPaymentService
{
public PaymentService(IOrderService order) { }
}
```
**更好的模式:** 通过将共享逻辑移至第三个协作者或更改依赖方向来打破循环,以便每个服务都有一个非循环的构造函数图。
**代码修复:** 否。打破依赖循环是一项设计变更。
## DI018: 不可实例化的实现类型
**捕获内容** 其实现类型无法由 DI 容器构造的注册,例如抽象类、接口、静态类、没有工厂注册的委托类型,或者没有公共构造函数的具体类。
**重要性:** 这些注册可以通过编译,但在容器尝试激活服务时会在运行时失败。
**问题:**
```
public interface IMyService { }
public sealed class BadPrivateCtorService : IMyService
{
private BadPrivateCtorService() { }
}
services.AddSingleton();
```
DI018 还会报告在没有工厂表达式的情况下用作实现类型的抽象类、接口、静态类和委托类型(例如 `services.AddSingleton()`,其中 `MyHandler` 是一个 `delegate`)。委托仅携带默认 DI 容器无法填充的隐式 `(object, IntPtr)` 和 `(object, UIntPtr)` 构造函数,因此注册会在激活时失败。
**更好的模式:**
```
public sealed class GoodConcreteService : IMyService { }
services.AddSingleton();
// For delegate types, register with a factory expression:
services.AddSingleton(sp => (msg) => Console.WriteLine(msg));
```
**代码修复:** 否。
## DI019: 从根提供程序解析范围服务
**捕获内容:** 从根 `IServiceProvider`(如 ASP.NET Core `app.Services`、ASP.NET test-host `factory.Services` / `server.Services`、Generic Host `host.Services`、可为空的根提供程序表面如 `app.Services!`,或由 `BuildServiceProvider()` 返回的提供程序)解析范围服务、已知的范围框架服务(如 `IOptionsSnapshot`)、来自 `AddDbContext(...)`、`AddDbContextFactory(...)`、`AddDbContextPool(...)` 和 `AddPooledDbContextFactory(...)`(包括服务/实现重载自注册)的 EF Core 上下文,或者其激活图到达范围服务的服务。
**重要性:** 默认容器的范围验证旨在防止直接或间接从根提供程序解析范围服务。从根解析它们可能会在运行时失败,或者意外地将范围状态延长到应用程序生命周期。
**问题:**
```
var app = builder.Build();
var db = app.Services.GetRequiredService();
```
**更好的模式:**
```
var app = builder.Build();
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
```
DI019 还会报告从注入的根提供程序解析范围服务的单例和托管服务方法。
**代码修复:** 否。创建正确的范围会改变控制流和释放语义,因此应慎重选择修复方式。
## 示例
- `samples/SampleApp`:`DI001` 到 `DI019` 的诊断示例。
- `samples/DI015InAction`:可运行的未解析依赖演示。
## 配置
在代码中抑制某个诊断:
```
#pragma warning disable DI007
var service = _provider.GetRequiredService();
#pragma warning restore DI007
```
或者在 `.editorconfig` 中:
```
[*.cs]
dotnet_diagnostic.DI007.severity = none
```
## 采用指南
- 如果您正在为团队或共享平台评估此包,请从 [docs/ADOPTION.md](docs/ADOPTION.md) 开始。
- 如果您想要一个可以从 issue、pull request 或内部文档链接的逐规则参考,请使用 [docs/RULES.md](docs/RULES.md)。
## 要求
- `.NET Standard 2.0` 消费者兼容性。
- `Microsoft.Extensions.DependencyInjection`。
## 已知限制
- 仅限编译时分析;无法分析运行时注册。
- 未完全跟踪跨程序集注册图。
- 生命周期推断遵循单一服务解析路径,可能无法为每个 `IEnumerable` 多注册激活路径建模。
## 常见问题
### 什么是 C# 的依赖注入生命周期分析器?
它是一个 Roslyn 分析器包,在编译期间检查您的 DI 注册和 DI 用法,以便在投入生产环境之前发现生命周期和范围错误。
### 此分析器能否防止 ASP.NET Core 运行时 DI 失败?
它有助于防止大量运行时失败,包括囚禁依赖、范围释放错误以及构造函数和工厂激活路径中未注册的直接/传递依赖。
### 这是否适用于 Rider、Visual Studio 和 CI 构建?
是的。它可以在 IDE 诊断和标准的 `dotnet build` / CI 工作流中运行,因为它是作为标准的 .NET 分析器包交付的。
## 贡献
请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 许可证
MIT 许可证 - 详见 [LICENSE](LICENSE)。标签:ASP.NET Core, BuildServiceProvider, DI, NuGet包, Roslyn, SOC Prime, 云安全监控, 代码分析, 作用域泄漏, 依赖注入, 内存泄漏, 凭证管理, 开发工具, 数据管道, 无运行时开销, 生命周期, 编译器分析, 软件工程, 错误检测, 零开销, 静态分析