如何在类库方法记录异常时获取调用项目/程序集名称?
我们团队现在用自定义NuGet包拆分ASP.NET Core 6 Web项目的通用代码,比如员工查询、工具类这类包。每个NuGet包的方法都会自行处理异常,把错误细节记录到数据库里,但现在有个头疼的问题——日志里只能看到NuGet包自身的信息(比如Location: Namespace.Employee -> EmpClass.cs --> methodName(parameter)),根本分不清是哪个Web项目调用时出的错,只能知道是NuGet包抛了异常。
这些辅助类都是静态类,如果要加项目名称参数,要么复制几十个方法,要么改现有方法导致旧项目直接报错(我们靠版本管理不想立刻改旧项目)。
我试过在NuGet包里用这段代码获取调用程序集,但结果总是指向NuGet包自己的DLL:
string callingAssembly = "Calling Assembly Not Available"; if( Assembly.GetCallingAssembly() != null ) { callingAssembly = Assembly.GetCallingAssembly().ToString(); }
想请教两个问题:
- 现在这种环境下,能不能获取到实际调用的Web项目名称?
- 从抽象设计层面,怎么重构才能让这类变更在未来更容易实现?
补充代码示例
调用端代码(Web项目里):
//User info是来自NuGet包的类库,DLL名为Intranet.Users。 UserInfo crashTest = UserInfo.GetUser(555);
NuGet包Intranet.Users中User.cs的方法:
public static UserInfo GetUser(int UserId) { try { //查询UserId=555的用户数据的代码 } catch (Exception ex) { string callingAssembly = "Calling Assembly Not Available"; if( Assembly.GetCallingAssembly() != null ) { callingAssembly = Assembly.GetCallingAssembly().ToString(); } //Logging类来自Intranet.Utilities NuGet包,用于将错误记录到数据库。 Logging.LogError(ex, "Intranet.Users > Users.cs > GetUser(UserId)", "UserId = " + UserId.ToString() + $" in {callingAssembly}" ); } }
这时callingAssembly的值是"Intranet.Users, Version=x.x.x.x, Culture=neutral, PublicKeyToken=null",完全不是我们要的调用项目名称。
一、获取调用项目名称的可行方案
Assembly.GetCallingAssembly()之所以返回NuGet包自身,是因为JIT优化或者方法内联的原因,尤其是静态方法的调用,可能会让调用栈被优化。这里有两个可靠的解决思路:
1. 调用栈追踪法
通过遍历调用栈帧,跳过当前NuGet包的程序集,找到第一个外部调用的项目程序集:
using System.Diagnostics; using System.Reflection; public static string GetCallingProjectName() { var stackTrace = new StackTrace(); foreach (var frame in stackTrace.GetFrames()) { var method = frame.GetMethod(); var assembly = method.DeclaringType.Assembly; // 替换成你的NuGet包程序集前缀,排除自身 if (!assembly.FullName.StartsWith("Intranet.Users")) { return assembly.GetName().Name; } } return "Calling Project Not Found"; }
注意:Release模式下如果开启了代码优化,部分调用帧可能会被省略。可以给这个方法加上[MethodImpl(MethodImplOptions.NoInlining)]标记,或者在NuGet包项目文件中添加<DebugType>full</DebugType>来避免内联优化。
2. 全局上下文注入法(更高效可靠)
利用ASP.NET Core的IWebHostEnvironment获取项目名称,让Web项目在启动时把这个值注入到NuGet包的静态上下文里:
- 在NuGet包中新增静态辅助类:
public static class AppContextHelper { public static string CurrentApplicationName { get; set; } = "Unknown"; }
- 在每个Web项目的
Program.cs中初始化:
var app = builder.Build(); AppContextHelper.CurrentApplicationName = app.Services.GetRequiredService<IWebHostEnvironment>().ApplicationName;
这种方式不受JIT优化影响,性能更好,只需要每个Web项目做一次初始化配置,后续NuGet包的方法直接读取AppContextHelper.CurrentApplicationName即可。
二、重构建议——让未来变更更易实现
静态类的设计虽然简单,但扩展性极差,遇到需要上下文信息的场景就会陷入两难。推荐从以下几个方向重构:
1. 改用依赖注入(DI)模式(最优方案)
放弃静态类,把NuGet包的功能改成可注入的服务,通过构造函数获取上下文信息:
- NuGet包中定义接口和实现:
public interface IUserService { UserInfo GetUser(int userId); } public class UserService : IUserService { private readonly string _applicationName; // 注入ASP.NET Core的环境信息,直接拿到项目名称 public UserService(IWebHostEnvironment env) { _applicationName = env.ApplicationName; } public UserInfo GetUser(int userId) { try { // 查询逻辑 } catch (Exception ex) { Logging.LogError(ex, "Intranet.Users > Users.cs > GetUser(UserId)", $"UserId = {userId} in {_applicationName}"); } } }
- Web项目中注册服务:
builder.Services.AddScoped<IUserService, UserService>();
这种方式彻底解决了静态类的扩展性问题,未来要添加任何上下文信息,都可以通过DI注入,不需要修改方法签名,也不会影响旧项目(只要旧项目不升级NuGet包版本即可)。
2. 上下文参数过渡方案(兼容旧项目)
如果暂时无法全量切换到DI,可以定义一个上下文对象,给方法添加可选参数:
public class CallContext { public string ApplicationName { get; set; } = "Unknown"; } // 保留原有方法,兼容旧项目 public static UserInfo GetUser(int userId) { return GetUser(userId, null); } // 新增带上下文的方法,供新项目使用 public static UserInfo GetUser(int userId, CallContext context) { var appName = context?.ApplicationName ?? GetCallingProjectName(); // 降级到调用栈方式 try { // 查询逻辑 } catch (Exception ex) { Logging.LogError(ex, "Intranet.Users > Users.cs > GetUser(UserId)", $"UserId = {userId} in {appName}"); } }
旧项目继续使用无参数方法,新项目可以传递上下文,逐步过渡到更优的设计。
3. 统一日志上下文
如果使用ASP.NET Core自带的ILogger日志系统,可以在Web项目中设置日志作用域:
var env = builder.Environment; using (logger.BeginScope("ApplicationName={Name}", env.ApplicationName)) { // 项目启动后的逻辑 }
NuGet包中也改用ILogger替代自定义Logging类,日志会自动带上这个作用域信息,不需要额外传递参数,这也是ASP.NET生态推荐的日志追踪方式。
内容的提问来源于stack exchange,提问作者jason

