diff --git a/AGENTS.md b/AGENTS.md index 3482eff..dbbe7b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,14 +154,15 @@ dotnet test DesignPatterns.slnx -c Release | DP063 | Composite tree max depth exceeded(生成器;`[CompositeSchema(MaxDepth)]` 约束被超过 Warning) | | DP064 | Composite child type not allowed(生成器;子节点实现类型不在父节点 `AllowedChildTypes` 集合中 Error) | | DP065 | Composite node count exceeded(生成器;`[CompositeSchema(MaxNodes)]` 约束被超过 Warning) | +| DP066 | Factory delegate captive dependency(Analyzer;Singleton 工厂委托内解析 Scoped/Transient 服务 Warning) | 常量:[`DesignPatterns.Diagnostics/DiagnosticIds.cs`](DesignPatterns.Diagnostics/DiagnosticIds.cs)。规则表:[`DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md`](DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md)。 诊断 ID 规范(**本表为唯一登记源**,其他文档不得另立分类): -- 下一个可用 ID:**DP066**;ID 一经发布不复用、不改语义。 +- 下一个可用 ID:**DP067**;ID 一经发布不复用、不改语义(DP067–DP071 已由 [RFC: Singleton 生命周期诊断](docs/rfc/SingletonLifecycleDiagnostics.md) 预留)。 - 新增 / 修改诊断必须同步 [`DiagnosticIds.cs`](DesignPatterns.Diagnostics/DiagnosticIds.cs)、[`DesignPatternsDiagnosticDescriptors.cs`](DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs)(经 Compile Link 编入 SourceGenerators / Analyzers)与 [`AnalyzerReleases.Unshipped.md`](DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md)。 -- 归属:DP006 / DP023 / DP024 / DP025 / DP033 / DP036 / DP044 / DP060 / DP061 / DP062 属 **Analyzer**;其余属**生成器**。 +- 归属:DP006 / DP023 / DP024 / DP025 / DP033 / DP036 / DP044 / DP060 / DP061 / DP062 / DP066 属 **Analyzer**;其余属**生成器**。 - 文案:`messageFormat` 须含可操作建议;`description` 供 IDE 悬停;`helpLinkUri` 指向 [`DesignPatterns.Docs` diagnostics 页](https://skymly.github.io/DesignPatterns.Docs/diagnostics)(`#dp###` 片段,见 [`DiagnosticHelpLinks.cs`](DesignPatterns.Diagnostics/DiagnosticHelpLinks.cs))。 - DP038(action 方法非 static)因 C# 编译器 CS0708 先于生成器拒绝 static 类中的实例成员,无法通过生成器测试触发;诊断保留供完整性。 diff --git a/CHANGELOG.md b/CHANGELOG.md index ead80ac..7f9aebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Composite parallel traversal**: `CompositeTraverser.TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` — parallel tree traversal with `MaxDegreeOfParallelism` and `MaxParallelDepth` options. BFS same-level parallel, DFS child-parallel recursion with sequential fallback beyond depth threshold. `AggregateException` for error aggregation. `ConfigureAwait(false)` for async paths. `#if` split: `Parallel.ForEachAsync` on net8.0, `SemaphoreSlim` + `Task.WhenAll` on netstandard2.0. 17 new tests. Design RFC: [docs/rfc/CompositeParallelTraversal.md](docs/rfc/CompositeParallelTraversal.md). - **DP062 Phase 2 — generated RegisterDi coverage**: `CaptiveDependencyAnalyzer` now also scans `RegisterDi` calls from DesignPatterns source generators. Extracts `implementationLifetime` (or `lifetime` for single-param overloads) and applies it to all types bearing `[RegisterStrategy]`, `[RegisterFactory]`, `[RegisterEventHandler]`, `[Decorator]`, `[CompositePart]` attributes. 5 new Verify snapshot tests. +- **Singleton lifecycle diagnostics P1 — Autofac and factory delegate coverage**: `CaptiveDependencyAnalyzer` now collects Autofac registrations (`RegisterType` + fluent lifetime chain, `Register(c => ...)`) via symbol-name matching with no Autofac package reference, and reports the new **DP066** (Warning) when a Singleton factory delegate (`AddSingleton(sp => ...)` or Autofac `Register(...).SingleInstance()`) directly resolves a Scoped/Transient service via `GetRequiredService`/`GetService`/`Resolve`. MSDI factory and instance registrations now also feed the lifetime map, so Singletons constructor-depending on factory-registered Scoped/Transient services report DP062. Limitation: only direct resolution calls inside the delegate body are detected. 11 new Verify snapshot tests. RFC: [docs/rfc/SingletonLifecycleDiagnostics.md](docs/rfc/SingletonLifecycleDiagnostics.md), ADR-008. ## [0.2.2] - 2026-06-30 diff --git a/DesignPatterns.Analyzers/AnalyzerReleases.Unshipped.md b/DesignPatterns.Analyzers/AnalyzerReleases.Unshipped.md index c903787..9128640 100644 --- a/DesignPatterns.Analyzers/AnalyzerReleases.Unshipped.md +++ b/DesignPatterns.Analyzers/AnalyzerReleases.Unshipped.md @@ -2,3 +2,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- +DP066 | DesignPatterns.Analyzers | Warning | Singleton factory delegate captures shorter-lived service diff --git a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs index 7f66b83..4b59fe2 100644 --- a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs +++ b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -11,7 +12,11 @@ namespace DesignPatterns.Analyzers; /// /// Reports DP062 when a Singleton service's constructor depends on a -/// Scoped or Transient service, creating a captive dependency. +/// Scoped or Transient service, creating a captive dependency, and +/// DP066 when a Singleton factory delegate resolves a Scoped or +/// Transient service. Covers MSDI registrations, generated RegisterDi +/// calls, and Autofac registrations (matched by symbol name, no +/// Autofac package reference). /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class CaptiveDependencyAnalyzer : DiagnosticAnalyzer @@ -19,8 +24,11 @@ public sealed class CaptiveDependencyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor Rule = DesignPatternsDiagnosticDescriptors.CaptiveDependency; + private static readonly DiagnosticDescriptor FactoryDelegateRule = + DesignPatternsDiagnosticDescriptors.FactoryDelegateCaptiveDependency; + public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(Rule); + ImmutableArray.Create(Rule, FactoryDelegateRule); public override void Initialize(AnalysisContext context) { @@ -55,11 +63,32 @@ private sealed class RegistrationEntry public INamedTypeSymbol ImplementationType { get; set; } = null!; public Lifetime Lifetime { get; set; } public InvocationExpressionSyntax Invocation { get; set; } = null!; + + /// + /// True for factory-delegate and instance registrations: the container + /// never invokes the implementation constructor, so constructor analysis + /// does not apply (the type still participates in the lifetime map). + /// + public bool SkipConstructorAnalysis { get; set; } + } + + /// + /// A Singleton factory delegate whose body is analyzed at compilation end + /// for resolutions of shorter-lived services (DP066). + /// + private sealed class FactoryDelegateEntry + { + public INamedTypeSymbol ServiceType { get; set; } = null!; + public AnonymousFunctionExpressionSyntax Lambda { get; set; } = null!; + + /// Semantic model of the lambda's tree, captured at collection time. + public SemanticModel SemanticModel { get; set; } = null!; } private static void OnCompilationStart(CompilationStartAnalysisContext context) { var registrations = new List(); + var factoryDelegates = new List(); // Phase 2: Pre-scan all types for DesignPatterns registration attributes. // Types are grouped by category so each RegisterDi call only applies @@ -67,11 +96,11 @@ private static void OnCompilationStart(CompilationStartAnalysisContext context) var attributedTypesByCategory = CollectAttributedImplementationTypes(context.Compilation); context.RegisterSyntaxNodeAction( - syntaxContext => CollectRegistration(syntaxContext, registrations, attributedTypesByCategory), + syntaxContext => CollectRegistration(syntaxContext, registrations, factoryDelegates, attributedTypesByCategory), SyntaxKind.InvocationExpression); context.RegisterCompilationEndAction( - endContext => AnalyzeRegistrations(endContext, registrations)); + endContext => AnalyzeRegistrations(endContext, registrations, factoryDelegates)); } /// @@ -132,6 +161,7 @@ private static Dictionary> Colle private static void CollectRegistration( SyntaxNodeAnalysisContext context, List registrations, + List factoryDelegates, Dictionary> attributedTypesByCategory) { var invocation = (InvocationExpressionSyntax)context.Node; @@ -145,7 +175,7 @@ private static void CollectRegistration( if (methodName is "AddSingleton" or "AddScoped" or "AddTransient") { - CollectDirectRegistration(context, invocation, methodName, registrations); + CollectDirectRegistration(context, invocation, methodName, registrations, factoryDelegates); } else if (methodName == "TryAdd") { @@ -155,13 +185,22 @@ private static void CollectRegistration( { CollectRegisterDiRegistration(context, invocation, attributedTypesByCategory, registrations); } + else if (methodName == "RegisterType") + { + CollectAutofacRegisterType(context, invocation, registrations); + } + else if (methodName == "Register") + { + CollectAutofacRegisterDelegate(context, invocation, registrations, factoryDelegates); + } } private static void CollectDirectRegistration( SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation, string methodName, - List registrations) + List registrations, + List factoryDelegates) { var lifetime = methodName switch { @@ -190,13 +229,36 @@ private static void CollectDirectRegistration( else if (methodSymbol.TypeArguments.Length == 1 && methodSymbol.TypeArguments[0] is INamedTypeSymbol singleGeneric) { - // Skip if the single arg has a lambda or instance argument (factory/instance registration) - if (args.Count > 0) + // Factory/instance registration: the container never calls the + // constructor, so skip constructor analysis but keep the type in + // the lifetime map. Singleton factory lambdas are analyzed for DP066. + if (args.Count > 0 && IsFactoryOrInstanceArg(args[0], context.SemanticModel)) { - if (IsFactoryOrInstanceArg(args[0], context.SemanticModel)) + if (IsOpenGeneric(singleGeneric)) { return; } + + registrations.Add(new RegistrationEntry + { + ImplementationType = singleGeneric, + Lifetime = lifetime, + Invocation = invocation, + SkipConstructorAnalysis = true, + }); + + if (lifetime == Lifetime.Singleton && + args[0].Expression is AnonymousFunctionExpressionSyntax lambda) + { + factoryDelegates.Add(new FactoryDelegateEntry + { + ServiceType = singleGeneric, + Lambda = lambda, + SemanticModel = context.SemanticModel, + }); + } + + return; } implType = singleGeneric; @@ -207,9 +269,7 @@ private static void CollectDirectRegistration( return; } - // Skip open generics - if (implType.IsUnboundGenericType || - (implType.TypeParameters.Length > 0 && implType.IsDefinition)) + if (IsOpenGeneric(implType)) { return; } @@ -222,6 +282,147 @@ private static void CollectDirectRegistration( }); } + /// + /// Collects Autofac RegisterType<TImpl>() / RegisterType(typeof(TImpl)) + /// registrations. Autofac methods are matched by containing namespace + /// (no Autofac package reference). The lifetime is resolved from the + /// fluent chain (SingleInstance etc.); Autofac defaults to + /// InstancePerDependency (Transient). + /// + private static void CollectAutofacRegisterType( + SyntaxNodeAnalysisContext context, + InvocationExpressionSyntax invocation, + List registrations) + { + var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (!IsAutofacMethod(methodSymbol)) + { + return; + } + + INamedTypeSymbol? implType = null; + if (methodSymbol!.TypeArguments.Length == 1 && + methodSymbol.TypeArguments[0] is INamedTypeSymbol generic) + { + implType = generic; + } + else + { + var args = invocation.ArgumentList?.Arguments ?? new SeparatedSyntaxList(); + if (args.Count == 1) + { + implType = ResolveTypeofArg(args[0].Expression, context.SemanticModel); + } + } + + if (implType is null || IsOpenGeneric(implType)) + { + return; + } + + registrations.Add(new RegistrationEntry + { + ImplementationType = implType, + Lifetime = ResolveAutofacChainLifetime(invocation), + Invocation = invocation, + }); + } + + /// + /// Collects Autofac Register(c => ...) delegate registrations. + /// The container never calls the implementation constructor, so the entry + /// only feeds the lifetime map; Singleton delegates are analyzed for DP066. + /// + private static void CollectAutofacRegisterDelegate( + SyntaxNodeAnalysisContext context, + InvocationExpressionSyntax invocation, + List registrations, + List factoryDelegates) + { + var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (!IsAutofacMethod(methodSymbol)) + { + return; + } + + if (methodSymbol!.TypeArguments.Length != 1 || + methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType || + IsOpenGeneric(serviceType)) + { + return; + } + + var args = invocation.ArgumentList?.Arguments ?? new SeparatedSyntaxList(); + if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax lambda) + { + return; + } + + var lifetime = ResolveAutofacChainLifetime(invocation); + + registrations.Add(new RegistrationEntry + { + ImplementationType = serviceType, + Lifetime = lifetime, + Invocation = invocation, + SkipConstructorAnalysis = true, + }); + + if (lifetime == Lifetime.Singleton) + { + factoryDelegates.Add(new FactoryDelegateEntry + { + ServiceType = serviceType, + Lambda = lambda, + SemanticModel = context.SemanticModel, + }); + } + } + + private static bool IsAutofacMethod(IMethodSymbol? methodSymbol) => + methodSymbol?.ContainingNamespace?.ToDisplayString() + .StartsWith("Autofac", StringComparison.Ordinal) == true; + + /// + /// Walks up the fluent chain from an Autofac registration call and returns + /// the declared lifetime. Intermediate calls (e.g. As<T>()) + /// are skipped; without an explicit lifetime call Autofac defaults to + /// InstancePerDependency (Transient). + /// + private static Lifetime ResolveAutofacChainLifetime(InvocationExpressionSyntax registrationCall) + { + var lifetime = Lifetime.Transient; + SyntaxNode node = registrationCall; + + while (node.Parent is MemberAccessExpressionSyntax memberAccess && + memberAccess.Parent is InvocationExpressionSyntax parentInvocation) + { + switch (memberAccess.Name.Identifier.ValueText) + { + case "SingleInstance": + lifetime = Lifetime.Singleton; + break; + case "InstancePerLifetimeScope": + case "InstancePerMatchingLifetimeScope": + case "InstancePerRequest": + case "InstancePerOwned": + lifetime = Lifetime.Scoped; + break; + case "InstancePerDependency": + lifetime = Lifetime.Transient; + break; + } + + node = parentInvocation; + } + + return lifetime; + } + + private static bool IsOpenGeneric(INamedTypeSymbol type) => + type.IsUnboundGenericType || + (type.TypeParameters.Length > 0 && type.IsDefinition); + private static void CollectTryAddRegistration( SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation, @@ -437,7 +638,8 @@ private static void CollectRegisterDiRegistration( private static void AnalyzeRegistrations( CompilationAnalysisContext context, - List registrations) + List registrations, + List factoryDelegates) { if (registrations.Count == 0) { @@ -456,13 +658,80 @@ private static void AnalyzeRegistrations( // For each Singleton, check constructor parameters. foreach (var reg in registrations) { - if (reg.Lifetime != Lifetime.Singleton) + if (reg.Lifetime != Lifetime.Singleton || reg.SkipConstructorAnalysis) { continue; } AnalyzeSingleton(context, reg.ImplementationType, reg.Invocation, lifetimeMap); } + + // For each Singleton factory delegate, check resolved services (DP066). + foreach (var factory in factoryDelegates) + { + AnalyzeFactoryDelegate(context, factory, lifetimeMap); + } + } + + /// + /// Scans a Singleton factory delegate body for direct + /// GetRequiredService<X> / GetService<X> (MSDI) or + /// Resolve<X> (Autofac) calls and reports DP066 when the + /// resolved service is registered as Scoped or Transient. Indirect + /// resolutions (helper methods, method groups) are not detected — + /// documented limitation. + /// + private static void AnalyzeFactoryDelegate( + CompilationAnalysisContext context, + FactoryDelegateEntry factory, + Dictionary lifetimeMap) + { + var semanticModel = factory.SemanticModel; + + foreach (var invocation in factory.Lambda.DescendantNodes().OfType()) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var name = memberAccess.Name.Identifier.ValueText; + if (name is not ("GetRequiredService" or "GetService" or "Resolve")) + { + continue; + } + + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol || + !IsServiceResolutionMethod(methodSymbol)) + { + continue; + } + + if (methodSymbol.TypeArguments.Length != 1 || + methodSymbol.TypeArguments[0] is not INamedTypeSymbol resolvedType) + { + continue; + } + + if (lifetimeMap.TryGetValue(resolvedType, out var resolvedLifetime) && + resolvedLifetime is Lifetime.Scoped or Lifetime.Transient) + { + context.ReportDiagnostic(Diagnostic.Create( + FactoryDelegateRule, + invocation.GetLocation(), + factory.ServiceType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + resolvedLifetime.ToString(), + resolvedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + } + + private static bool IsServiceResolutionMethod(IMethodSymbol methodSymbol) + { + var ns = methodSymbol.ContainingNamespace?.ToDisplayString(); + return ns is not null && + (ns.StartsWith("Microsoft.Extensions.DependencyInjection", StringComparison.Ordinal) || + ns.StartsWith("Autofac", StringComparison.Ordinal)); } private static void AnalyzeSingleton( diff --git a/DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj b/DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj index bffe4cb..41c4aa4 100644 --- a/DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj +++ b/DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj @@ -8,14 +8,14 @@ false false DesignPatterns.Analyzers - - $(NoWarn);RS2003 + + $(NoWarn);RS2002;RS2003 diff --git a/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs b/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs index 56fd507..68a12a9 100644 --- a/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs +++ b/DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs @@ -335,6 +335,16 @@ public static class DesignPatternsDiagnosticDescriptors DiagnosticSeverity.Warning, AnalyzerCategory); + // Factory delegate captive dependency (DP066) + + public static DiagnosticDescriptor FactoryDelegateCaptiveDependency { get; } = Create( + DiagnosticIds.FactoryDelegateCaptiveDependency, + "Singleton factory delegate captures shorter-lived service", + "Singleton factory delegate for '{0}' resolves {1} service '{2}'. The resolved instance is captured for the singleton's lifetime and effectively becomes Singleton. Register '{2}' as Singleton, or resolve it per operation instead of inside the factory delegate.", + "A factory delegate registered as Singleton runs once; any Scoped or Transient service it resolves is captured for the singleton's lifetime. This is a silent DI configuration error.", + DiagnosticSeverity.Warning, + AnalyzerCategory); + // Composite tree schema validation (DP063–DP065) public static DiagnosticDescriptor CompositeTreeMaxDepthExceeded { get; } = Create( diff --git a/DesignPatterns.Diagnostics/DiagnosticIds.cs b/DesignPatterns.Diagnostics/DiagnosticIds.cs index bbec1d0..d5259f8 100644 --- a/DesignPatterns.Diagnostics/DiagnosticIds.cs +++ b/DesignPatterns.Diagnostics/DiagnosticIds.cs @@ -70,4 +70,5 @@ public static class DiagnosticIds public const string CompositeTreeMaxDepthExceeded = "DP063"; public const string CompositeChildTypeNotAllowed = "DP064"; public const string CompositeNodeCountExceeded = "DP065"; + public const string FactoryDelegateCaptiveDependency = "DP066"; } diff --git a/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md b/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md index 5b1724b..552e61c 100644 --- a/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -67,3 +67,4 @@ DP062 | DesignPatterns.Analyzers | Warning | Singleton captive dependency on DP063 | DesignPatterns.Generators | Warning | Composite tree max depth exceeded DP064 | DesignPatterns.Generators | Error | Composite child type not allowed by parent AllowedChildTypes DP065 | DesignPatterns.Generators | Warning | Composite node count exceeds MaxNodes limit +DP066 | DesignPatterns.Analyzers | Warning | Singleton factory delegate captures shorter-lived service diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 612537e..ef634ab 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -20,7 +20,7 @@ | Event Aggregator | `{Event}EventHandlerRegistry` | | State | `{StateEnum}TransitionTable`、partial `{Holder}` 便捷方法 | -新增生成器必须沿用此命名风格(详见 [Decorator.md](design/Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP066**(DP063–DP065 为 Composite 树 schema 校验,DP060–DP062 为 DI 生命周期校验,DP056–DP059 为 State hierarchy,DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。 +新增生成器必须沿用此命名风格(详见 [Decorator.md](design/Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP067**,DP067–DP071 已由 [Singleton 生命周期诊断 RFC](rfc/SingletonLifecycleDiagnostics.md) 预留(DP066 为 Singleton 工厂委托 captive dependency,DP063–DP065 为 Composite 树 schema 校验,DP060–DP062 为 DI 生命周期校验,DP056–DP059 为 State hierarchy,DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。 诊断 ID 预分配(F2+ 增强项,登记后不提前占用,实现时按序领取): diff --git a/docs/plans/SingletonLifecycleDiagnostics.md b/docs/plans/SingletonLifecycleDiagnostics.md index a14d262..c2a4346 100644 --- a/docs/plans/SingletonLifecycleDiagnostics.md +++ b/docs/plans/SingletonLifecycleDiagnostics.md @@ -21,7 +21,7 @@ | 阶段 | 内容 | 模块(AGENTS.md 边界) | 状态 | PR | |------|------|------------------------|------|-----| | **P0** | RFC Draft → 设计评审([Review](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md))→ Accepted → [ADR-008](../adr/ADR-008-singleton-lifecycle-diagnostics.md) | Docs | [x] | (直接提交) | -| **P1** | 扩展 captive dependency:Autofac / RegisterAutofac 注册收集(符号名匹配)、工厂委托 lambda 分析;DP066;CaptiveDependencyAnalyzer 测试 | Analyzers | [ ] | — | +| **P1** | 扩展 captive dependency:Autofac / RegisterAutofac 注册收集(符号名匹配)、工厂委托 lambda 分析;DP066;CaptiveDependencyAnalyzer 测试 | Analyzers | [x] | (直接提交) | | **P2a** | `GenerateSingletonAttribute.InitializeAsync` + 生成 `GetInstanceAsync`(`Lazy>`)+ 生成器 DP067 | Runtime + SourceGenerators(拆 2 PR) | [ ] | — | | **P2b** | DP068 DI 混用警告 + DP069 ThreadSafe 提示 | Analyzers | [ ] | — | | **P2** | Spec `docs/spec/Singleton.md` + Design Doc + Sample 更新(可选) | Docs | [ ] | — | @@ -30,7 +30,7 @@ ## 验收标准 -- [ ] P1:Autofac `SingleInstance` 构造函数 captive + 工厂委托 captive(DP066)有 Verify 测试;DP066 已登记并发布到 `AnalyzerReleases` +- [x] P1:Autofac `SingleInstance` 构造函数 captive + 工厂委托 captive(DP066)有 Verify 测试;DP066 已登记并发布到 `AnalyzerReleases` - [ ] P2a:`InitializeAsync` 指定后生成 `GetInstanceAsync` 且不生成同步 `Instance`;无效签名报 DP067;netstandard2.0 / net8.0 测试通过 - [ ] P2b:同时 `[GenerateSingleton]` + `AddSingleton` 报 DP068;`ThreadSafe=false` 场景报 DP069 - [ ] P3:典型 static mutable singleton 样本报 DP070;与 DI 双重注册报 DP071;误报样本不报 @@ -51,3 +51,4 @@ |------|------|------| | 2026-07-08 | 初版 Plan 自 RFC Draft 创建 | 用户选定完整 RFC 三阶段范围 | | 2026-07-08 | P1 删除字段注入项(DP066 改为工厂委托 captive);P2a 改 `GetInstanceAsync` 设计 | 设计评审 Blocker #1/#2([Review](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)) | +| 2026-07-08 | P1 完成:Autofac 注册收集 + DP066 工厂委托分析 + 11 个 Verify 测试 | P1 实现落地 | diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.DoesNotReport_WhenAutofacSingleInstanceDependsOnSingleInstance.verified.txt b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.DoesNotReport_WhenAutofacSingleInstanceDependsOnSingleInstance.verified.txt new file mode 100644 index 0000000..01a9869 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.DoesNotReport_WhenAutofacSingleInstanceDependsOnSingleInstance.verified.txt @@ -0,0 +1 @@ +(no diagnostics) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacChainHasIntermediateCalls.verified.txt b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacChainHasIntermediateCalls.verified.txt new file mode 100644 index 0000000..e5127d7 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacChainHasIntermediateCalls.verified.txt @@ -0,0 +1 @@ +DP062 [Warning] Singleton service 'SingletonService' depends on Scoped service 'ScopedService'. The Scoped dependency will be captured by the Singleton and effectively become Singleton. Register 'ScopedService' as Singleton, or use a factory delegate to resolve 'ScopedService' per scope. @ Test.cs:(17,9) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnDefaultTransient.verified.txt b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnDefaultTransient.verified.txt new file mode 100644 index 0000000..4130893 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnDefaultTransient.verified.txt @@ -0,0 +1 @@ +DP062 [Warning] Singleton service 'SingletonService' depends on Transient service 'TransientService'. The Transient dependency will be captured by the Singleton and effectively become Singleton. Register 'TransientService' as Singleton, or use a factory delegate to resolve 'TransientService' per scope. @ Test.cs:(16,9) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnInstancePerLifetimeScope.verified.txt b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnInstancePerLifetimeScope.verified.txt new file mode 100644 index 0000000..fa21038 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenAutofacSingleInstanceDependsOnInstancePerLifetimeScope.verified.txt @@ -0,0 +1 @@ +DP062 [Warning] Singleton service 'SingletonService' depends on Scoped service 'ScopedService'. The Scoped dependency will be captured by the Singleton and effectively become Singleton. Register 'ScopedService' as Singleton, or use a factory delegate to resolve 'ScopedService' per scope. @ Test.cs:(16,9) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenSingletonDependsOnScopedFactoryRegistration.verified.txt b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenSingletonDependsOnScopedFactoryRegistration.verified.txt new file mode 100644 index 0000000..fa21038 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.ReportsDp062_WhenSingletonDependsOnScopedFactoryRegistration.verified.txt @@ -0,0 +1 @@ +DP062 [Warning] Singleton service 'SingletonService' depends on Scoped service 'ScopedService'. The Scoped dependency will be captured by the Singleton and effectively become Singleton. Register 'ScopedService' as Singleton, or use a factory delegate to resolve 'ScopedService' per scope. @ Test.cs:(16,9) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.cs b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.cs index 69c277f..57e89de 100644 --- a/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.cs +++ b/tests/DesignPatterns.Analyzers.Tests/CaptiveDependencyAnalyzerTests.cs @@ -737,4 +737,166 @@ public static void Configure(IServiceCollection services) await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); } + + [Fact] + public async Task ReportsDp062_WhenAutofacSingleInstanceDependsOnInstancePerLifetimeScope() + { + const string source = """ + using Autofac; + + namespace TestAssembly; + + public class ScopedService { } + public class SingletonService + { + public SingletonService(ScopedService scoped) { } + } + + public static class Startup + { + public static void Configure(ContainerBuilder builder) + { + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().SingleInstance(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); + } + + [Fact] + public async Task ReportsDp062_WhenAutofacSingleInstanceDependsOnDefaultTransient() + { + // Autofac defaults to InstancePerDependency (Transient) when no + // lifetime method is chained. + const string source = """ + using Autofac; + + namespace TestAssembly; + + public class TransientService { } + public class SingletonService + { + public SingletonService(TransientService transient) { } + } + + public static class Startup + { + public static void Configure(ContainerBuilder builder) + { + builder.RegisterType(); + builder.RegisterType().SingleInstance(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); + } + + [Fact] + public async Task ReportsDp062_WhenAutofacChainHasIntermediateCalls() + { + // The lifetime method appears after intermediate fluent calls (As()). + const string source = """ + using Autofac; + + namespace TestAssembly; + + public interface IWorker { } + public class ScopedService { } + public class SingletonService : IWorker + { + public SingletonService(ScopedService scoped) { } + } + + public static class Startup + { + public static void Configure(ContainerBuilder builder) + { + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().As().SingleInstance(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); + } + + [Fact] + public async Task DoesNotReport_WhenAutofacSingleInstanceDependsOnSingleInstance() + { + const string source = """ + using Autofac; + + namespace TestAssembly; + + public class DependencyService { } + public class SingletonService + { + public SingletonService(DependencyService dependency) { } + } + + public static class Startup + { + public static void Configure(ContainerBuilder builder) + { + builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); + } + + [Fact] + public async Task ReportsDp062_WhenSingletonDependsOnScopedFactoryRegistration() + { + // Factory-based registrations now feed the lifetime map, so a + // Singleton constructor-depending on a Scoped factory registration + // is a captive dependency. + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class ScopedService { } + public class SingletonService + { + public SingletonService(ScopedService scoped) { } + } + + public static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddScoped(sp => new ScopedService()); + services.AddSingleton(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP062")); + } } diff --git a/tests/DesignPatterns.Analyzers.Tests/DesignPatterns.Analyzers.Tests.csproj b/tests/DesignPatterns.Analyzers.Tests/DesignPatterns.Analyzers.Tests.csproj index d5460ee..ab6cc99 100644 --- a/tests/DesignPatterns.Analyzers.Tests/DesignPatterns.Analyzers.Tests.csproj +++ b/tests/DesignPatterns.Analyzers.Tests/DesignPatterns.Analyzers.Tests.csproj @@ -27,6 +27,9 @@ + + diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenResolveCallIsNotContainerMethod.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenResolveCallIsNotContainerMethod.verified.txt new file mode 100644 index 0000000..01a9869 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenResolveCallIsNotContainerMethod.verified.txt @@ -0,0 +1 @@ +(no diagnostics) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenScopedFactoryResolvesTransient.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenScopedFactoryResolvesTransient.verified.txt new file mode 100644 index 0000000..01a9869 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenScopedFactoryResolvesTransient.verified.txt @@ -0,0 +1 @@ +(no diagnostics) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenSingletonFactoryResolvesSingleton.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenSingletonFactoryResolvesSingleton.verified.txt new file mode 100644 index 0000000..01a9869 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.DoesNotReport_WhenSingletonFactoryResolvesSingleton.verified.txt @@ -0,0 +1 @@ +(no diagnostics) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenAutofacRegisterDelegateSingleInstanceResolvesScoped.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenAutofacRegisterDelegateSingleInstanceResolvesScoped.verified.txt new file mode 100644 index 0000000..a75261d --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenAutofacRegisterDelegateSingleInstanceResolvesScoped.verified.txt @@ -0,0 +1 @@ +DP066 [Warning] Singleton factory delegate for 'SingletonService' resolves Scoped service 'ScopedService'. The resolved instance is captured for the singleton's lifetime and effectively becomes Singleton. Register 'ScopedService' as Singleton, or resolve it per operation instead of inside the factory delegate. @ Test.cs:(16,52) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesScoped.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesScoped.verified.txt new file mode 100644 index 0000000..67d0b35 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesScoped.verified.txt @@ -0,0 +1 @@ +DP066 [Warning] Singleton factory delegate for 'SingletonService' resolves Scoped service 'ScopedService'. The resolved instance is captured for the singleton's lifetime and effectively becomes Singleton. Register 'ScopedService' as Singleton, or resolve it per operation instead of inside the factory delegate. @ Test.cs:(16,76) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesTransientViaGetService.verified.txt b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesTransientViaGetService.verified.txt new file mode 100644 index 0000000..d9ad325 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.ReportsDp066_WhenMsdiSingletonFactoryResolvesTransientViaGetService.verified.txt @@ -0,0 +1 @@ +DP066 [Warning] Singleton factory delegate for 'SingletonService' resolves Transient service 'TransientService'. The resolved instance is captured for the singleton's lifetime and effectively becomes Singleton. Register 'TransientService' as Singleton, or resolve it per operation instead of inside the factory delegate. @ Test.cs:(16,76) \ No newline at end of file diff --git a/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.cs b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.cs new file mode 100644 index 0000000..b4400f6 --- /dev/null +++ b/tests/DesignPatterns.Analyzers.Tests/FactoryDelegateCaptiveDependencyTests.cs @@ -0,0 +1,202 @@ +using DesignPatterns.Analyzers; +using VerifyTests; + +namespace DesignPatterns.Analyzers.Tests; + +/// +/// DP066: Singleton factory delegates that resolve Scoped/Transient services. +/// +public sealed class FactoryDelegateCaptiveDependencyTests +{ + [Fact] + public async Task ReportsDp066_WhenMsdiSingletonFactoryResolvesScoped() + { + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class ScopedService { } + public class SingletonService + { + public SingletonService(ScopedService scoped) { } + } + + public static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddScoped(); + services.AddSingleton(sp => new SingletonService(sp.GetRequiredService())); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } + + [Fact] + public async Task ReportsDp066_WhenMsdiSingletonFactoryResolvesTransientViaGetService() + { + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class TransientService { } + public class SingletonService + { + public SingletonService(TransientService? transient) { } + } + + public static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(sp => new SingletonService(sp.GetService())); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } + + [Fact] + public async Task ReportsDp066_WhenAutofacRegisterDelegateSingleInstanceResolvesScoped() + { + const string source = """ + using Autofac; + + namespace TestAssembly; + + public class ScopedService { } + public class SingletonService + { + public SingletonService(ScopedService scoped) { } + } + + public static class Startup + { + public static void Configure(ContainerBuilder builder) + { + builder.RegisterType().InstancePerLifetimeScope(); + builder.Register(c => new SingletonService(c.Resolve())).SingleInstance(); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } + + [Fact] + public async Task DoesNotReport_WhenSingletonFactoryResolvesSingleton() + { + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class DependencyService { } + public class SingletonService + { + public SingletonService(DependencyService dependency) { } + } + + public static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(sp => new SingletonService(sp.GetRequiredService())); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } + + [Fact] + public async Task DoesNotReport_WhenScopedFactoryResolvesTransient() + { + // Only Singleton factory delegates capture; a Scoped factory resolving + // Transient is not analyzed. + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class TransientService { } + public class ScopedService + { + public ScopedService(TransientService transient) { } + } + + public static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + services.AddScoped(sp => new ScopedService(sp.GetRequiredService())); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } + + [Fact] + public async Task DoesNotReport_WhenResolveCallIsNotContainerMethod() + { + // A method named Resolve/GetRequiredService outside MSDI/Autofac + // namespaces must not trigger DP066. + const string source = """ + using Microsoft.Extensions.DependencyInjection; + + namespace TestAssembly; + + public class ScopedService { } + public class SingletonService { } + + public class CustomLocator + { + public T Resolve() where T : new() => new T(); + } + + public static class Startup + { + public static void Configure(IServiceCollection services, CustomLocator locator) + { + services.AddScoped(); + services.AddSingleton(sp => locator.Resolve()); + } + } + """; + + var diagnostics = await AnalyzerTestContext.RunAnalyzersAsync( + source, + new CaptiveDependencyAnalyzer()); + + await Verifier.Verify(AnalyzerVerifyHelper.FormatDiagnostics(diagnostics, "DP066")); + } +} diff --git a/tests/DesignPatterns.Analyzers.Tests/UnregisteredStrategyAnalyzerTests.cs b/tests/DesignPatterns.Analyzers.Tests/UnregisteredStrategyAnalyzerTests.cs index 47b1b20..61aa98e 100644 --- a/tests/DesignPatterns.Analyzers.Tests/UnregisteredStrategyAnalyzerTests.cs +++ b/tests/DesignPatterns.Analyzers.Tests/UnregisteredStrategyAnalyzerTests.cs @@ -226,6 +226,7 @@ private static ImmutableArray CreateReferences() MetadataReference.CreateFromFile(typeof(ValueTask).Assembly.Location), MetadataReference.CreateFromFile(typeof(Behavioral.RegisterStrategyAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(ServiceLifetime).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Autofac.ContainerBuilder).Assembly.Location), }; var trustedPlatformAssemblies = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string; @@ -235,7 +236,8 @@ private static ImmutableArray CreateReferences() { if (assemblyPath.EndsWith("System.Runtime.dll", StringComparison.OrdinalIgnoreCase) || assemblyPath.EndsWith("netstandard.dll", StringComparison.OrdinalIgnoreCase) || - assemblyPath.EndsWith("System.Collections.dll", StringComparison.OrdinalIgnoreCase)) + assemblyPath.EndsWith("System.Collections.dll", StringComparison.OrdinalIgnoreCase) || + assemblyPath.EndsWith($"{Path.DirectorySeparatorChar}System.ComponentModel.dll", StringComparison.OrdinalIgnoreCase)) { references.Add(MetadataReference.CreateFromFile(assemblyPath)); }