1. 为什么.NET Core错误处理值得专门讨论?
在.NET Core开发中,错误处理看似基础却暗藏玄机。我见过太多团队在项目后期被诡异的异常折磨得焦头烂额,而问题根源往往是最初的错误处理设计存在缺陷。不同于传统的ASP.NET,.NET Core的中间件管道和依赖注入体系让错误处理既更灵活又更复杂。
最近帮一个电商平台排查线上故障时发现,他们的支付服务因为未正确处理AggregateException,导致在第三方API超时情况下错误地标记订单为"支付成功"。这个Bug潜伏了三个月才被发现,造成的财务损失超过六位数。这让我意识到,很多.NET Core开发者(包括曾经的我)都在重复踩同样的坑。
2. 五大致命陷阱深度解析
2.1 陷阱一:过度依赖全局异常过滤器
// 典型错误示范 services.AddControllers(options => { options.Filters.Add(new CustomExceptionFilter()); });全局异常过滤器看似一劳永逸,实则存在严重局限:
- 中间件异常无法捕获:在中间件管道中(如AuthenticationMiddleware)抛出的异常会直接跳过过滤器
- 异步上下文丢失:过滤器中的
async/await可能导致调用栈信息不完整 - 响应格式失控:无法根据
Accept头动态返回JSON/XML错误格式
实战建议:结合中间件和过滤器使用。用中间件处理基础架构异常,过滤器处理业务异常。
2.2 陷阱二:忽略CancellationToken传播
// 危险代码示例 public async Task ProcessDataAsync() { var data = await _httpClient.GetAsync("api/data"); // 未传递CancellationToken }在微服务架构中,这个问题尤为致命:
- 上游服务取消请求时,下游操作仍在执行
- 数据库连接池被挂起操作占满
- 负载均衡器认为服务无响应而触发熔断
正确做法应该像这样:
public async Task ProcessDataAsync(CancellationToken ct) { ct.ThrowIfCancellationRequested(); var data = await _httpClient.GetAsync("api/data", ct); await _dbContext.SaveChangesAsync(ct); }2.3 陷阱三:错误记录上下文缺失
常见反模式:
try { // ... } catch (Exception ex) { _logger.LogError(ex.Message); // 只记录Message }这种记录方式在排查分布式系统问题时几乎无用。我推荐的结构化日志应该包含:
_logger.LogError(ex, "Order processing failed for {OrderId} at {Stage}. User: {UserId}", order.Id, currentStage, HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));关键字段包括:
- 关联ID(CorrelationId)
- 操作上下文(如当前处理的实体ID)
- 用户身份信息
- 环境变量(仅开发环境)
2.4 陷阱四:异常类型处理不当
.NET Core中最危险的异常类型:
| 异常类型 | 典型陷阱 | 正确处理方式 |
|---|---|---|
| AggregateException | 直接catch导致信息丢失 | Flatten()后处理内部异常 |
| TaskCanceledException | 与TimeoutException混淆 | 检查CancellationToken |
| DbUpdateConcurrencyException | 简单重试导致死循环 | 采用乐观并发控制策略 |
特别提醒:处理System.Text.Json序列化异常时,JsonException的Path属性能精确定位出错位置:
catch (JsonException jex) { _logger.LogError($"JSON parsing failed at {jex.Path}"); return BadRequest(new { ErrorPath = jex.Path, Message = "Invalid data format" }); }2.5 陷阱五:生产环境过度暴露细节
安全反模式示例:
app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { var exception = context.Features.Get<IExceptionHandlerFeature>(); await context.Response.WriteAsync(exception.Error.ToString()); // 暴露堆栈 }); });根据OWASP建议,应该实现分级错误响应:
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/error"); app.UseStatusCodePagesWithReExecute("/error/{0}"); } // 在ErrorController中 public IActionResult Index(int? statusCode = null) { var model = new ErrorViewModel { RequestId = Activity.Current?.Id, IsProduction = !_env.IsDevelopment() }; if(statusCode.HasValue) { model.StatusCode = statusCode.Value; if(statusCode == 404) model.Message = "Resource not found"; } return View(model); }3. 高级防御性编程技巧
3.1 熔断模式实现
Polly库的典型配置:
services.AddHttpClient("PaymentGateway") .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))) .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(10));关键参数经验值:
- 重试间隔:指数退避(2^n秒)
- 熔断阈值:连续5次失败
- 超时设置:根据SLA的P99响应时间设定
3.2 健康检查集成
ASP.NET Core的健康检查端点常被忽视:
services.AddHealthChecks() .AddSqlServer(_config["ConnectionStrings:Default"]) .AddRedis(_config["Redis:Configuration"]) .AddUrlGroup(new Uri("https://payment-gateway/api/health"), "PaymentGateway"); app.UseHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; await context.Response.WriteAsync( JsonSerializer.Serialize(new { Status = report.Status.ToString(), Checks = report.Entries.Select(e => new { Service = e.Key, Status = e.Value.Status.ToString(), Duration = e.Value.Duration.TotalMilliseconds }) })); } });4. 诊断工具链推荐
4.1 本地调试神器
- Exception Helper(VS扩展):自动建议可能的异常处理方案
- OzCode:可视化异常传播路径
dotnet-trace:捕获生产环境异常堆栈
4.2 生产环境监控
# 使用DiagnosticSource监听异常 dotnet counters monitor System.Runtime Microsoft.AspNetCore.Hosting -p <PID>推荐报警阈值设置:
- 异常频率:>50次/分钟
- 错误率:>5%的请求
- 超时比例:>1%的请求
5. 我的血泪经验
在金融项目中,我们曾因为未处理OperationCanceledException导致对账服务漏单。后来我们建立了错误处理检查清单:
- 所有async方法必须接受CancellationToken
- 日志必须包含CorrelationId
- 第三方调用必须配置熔断
- 数据库操作必须设置超时(建议5-10秒)
- 生产环境错误响应必须标准化
最近帮客户重构错误处理系统时,通过引入ElasticSearch存储错误日志,配合Kibana的异常模式分析,将MTTR(平均修复时间)从4小时缩短到15分钟。关键是在错误发生的第一时间就能获取完整的上下文信息。