ASP.NET Core如何检测哪个服务正在调用已弃用的服务接口
问题说明
ASP.NET Core开发中,已注册的第三方库服务存在依赖某标记为弃用的接口服务的情况,无法定位具体是哪些服务、哪个第三方库在调用该弃用接口。
已完成的排查操作:
- 为弃用服务接口编写自定义实现类
- 将自定义实现注册到DI依赖注入容器
对应实现代码:
// Registration services.AddTransient<IServiceInterface>((services) => new CustomService(Log.Logger)); // Custom implementation internal class CustomService : IServiceInterface { public CustomService (ILogger logger) { logger.Warning("!!! CustomService is still being used !!!"); } }
该方案仅能确认弃用服务确实存在调用,无法定位该弃用服务被哪个服务依赖、为哪个服务创建实例。后续尝试在自定义实现的构造函数中抓取堆栈信息定位调用方:
var st = new System.Diagnostics.StackTrace(); logger.Warning("!!! CustomService is still being used !!!" + Environment.NewLine + "{stacktrace}", st.ToString());
测试发现抓取到的堆栈信息未包含依赖该弃用服务的目标服务内容,无法完成定位。
可行解决方案
静态依赖扫描(优先使用,无需运行触发)
在调用BuildServiceProvider之前,所有服务注册信息都已加载到服务集合中,可以直接反射扫描所有已注册服务的构造函数依赖,批量找出静态声明依赖弃用接口的服务,覆盖绝大多数第三方库的常规注册场景。
实现代码:
var deprecatedServiceType = typeof(IServiceInterface); var dependentServiceList = new List<(Type serviceType, Type implType, ServiceLifetime lifetime)>(); foreach (var serviceDescriptor in services) { // 跳过工厂注册、实例注册的服务,这类服务后续用运行时方案排查 if (serviceDescriptor.ImplementationType == null) continue; // 查找实现类的公开实例构造函数 var ctors = serviceDescriptor.ImplementationType.GetConstructors(BindingFlags.Instance | BindingFlags.Public); foreach (var ctor in ctors) { // 检查构造函数参数是否包含弃用接口 var hasDeprecatedDependency = ctor.GetParameters() .Any(param => param.ParameterType == deprecatedServiceType); if (hasDeprecatedDependency) { dependentServiceList.Add(( serviceDescriptor.ServiceType, serviceDescriptor.ImplementationType, serviceDescriptor.Lifetime )); break; } } } // 遍历输出所有静态依赖的服务信息 foreach (var item in dependentServiceList) { Console.WriteLine($"找到弃用接口依赖:服务类型{item.serviceType.FullName},实现类型{item.implType.FullName},生命周期{item.lifetime}"); }
运行时链路追踪(覆盖动态解析、工厂注册场景)
之前在自定义服务构造函数中抓不到完整堆栈,核心原因是默认DI容器基于表达式树动态编译服务激活逻辑,Release模式下会优化掉大量中间栈帧,需要调整抓取位置和DI配置获取完整链路:
- 调整堆栈抓取位置:不要在自定义服务的构造函数中抓栈,要在注册弃用服务的工厂委托入口第一时间抓取,此时DI容器刚触发该服务的解析逻辑,栈帧中保留了父级依赖的激活信息
- 临时关闭DI的编译优化,使用反射模式激活服务,避免栈帧被优化
修改后的注册代码:
// 调整弃用服务的注册逻辑 services.AddTransient<IServiceInterface>(sp => { // 工厂入口第一时间抓取堆栈,跳过1层当前委托的栈帧 var trackStack = new System.Diagnostics.StackTrace(skipFrames: 1, fNeedFileInfo: true); var trackLogger = sp.GetRequiredService<ILogger<CustomService>>(); trackLogger.Warning("检测到弃用接口IServiceInterface被解析,完整调用栈:{TrackStack}", trackStack.ToString()); return new CustomService(trackLogger); }); // 构建服务提供者时关闭编译优化 var sp = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, // .NET 7+ 支持该配置,强制使用反射逻辑激活服务,保留完整栈帧 UseMemberAccessResolution = false });
如果是.NET 6及以下版本不支持UseMemberAccessResolution配置,可以搭配DI诊断订阅获取解析链路:
// 全局订阅DI诊断事件 DiagnosticListener.AllListeners.Subscribe(listener => { if (listener.Name.Equals("Microsoft.Extensions.DependencyInjection")) { listener.Subscribe(eventPayload => { if (eventPayload.Key.Equals("ServiceResolved")) { dynamic payload = eventPayload.Value; if (payload.ServiceType == typeof(IServiceInterface)) { Console.WriteLine($"弃用接口被服务{payload.ParentServiceType?.FullName}解析依赖"); } } }); } });
如果存在手动调用GetService<IServiceInterface>()动态解析的场景,该方案抓取的堆栈会直接包含手动调用的方法名和所属类,可直接定位。
另外可以临时将DI系统的日志级别调整为Debug,.NET Core 3.0+的内置DI会在Debug级别输出完整的服务解析链路,直接在日志中筛选IServiceInterface相关的解析记录,即可快速定位依赖方。
内容的提问来源于stack exchange,提问作者R. Hoek
相关产品推荐
相关产品推荐

