当前位置: 首页 > news >正文

C# 异常处理与拦截全攻略:try/catch/finally、using、ASP.NET Core 中间件与过滤器一网打尽(含完整示例)

C# 异常处理与拦截全攻略:try/catch/finally、using、ASP.NET Core 中间件与过滤器一网打尽(含完整示例)

面向:.NET 后端/全栈工程师、API 开发者、需要做统一异常处理与日志追踪的同学。
亮点:系统化讲清楚 try/catch/finally 的执行细节与坑、using/await using 的正确姿势、ASP.NET Core 中间件过滤器的“全链路拦截”,送上可复制落地代码与最佳实践清单。


目录

  1. 异常模型与基本原则
  2. try/catch/finally:执行顺序、隐藏陷阱与正确用法
  3. using / await using:资源释放的终极武器
  4. ASP.NET Core 中间件拦截:全局异常与日志统一出口
  5. MVC 过滤器拦截:动作/结果/异常的精细化处理
  6. (可选)Minimal API 的 Endpoint Filters
  7. 日志与追踪:结构化日志 + 关联ID(CorrelationId)
  8. 常见反模式与最佳实践清单
  9. 完整代码清单与项目骨架

异常模型与基本原则

.NET 中异常的本质

  • 异常(Exception)是不可预期不应在正常流程中出现的错误。
  • 异常是栈展开(stack unwinding)过程中被抛出并一路向外传播,直到被某个 catch 捕获;若没人捕获,进程/请求终结。

基本原则

  1. 就近处理、集中兜底:业务层就近处理明确可恢复的异常;框架层用中间件做全局兜底。
  2. 不要吞异常:捕获后必须记录、转换或重抛,避免“悄无声息”。
  3. 抛出语义化异常:用自定义业务异常或标准异常族,携带上下文信息。
  4. 不要把异常当分支控制:异常只用于异常路径。

try/catch/finally:执行顺序、隐藏陷阱与正确用法

执行顺序

  • 正常:tryfinally
  • 有异常并被捕获:trycatchfinally
  • 有异常但未被捕获:tryfinally → 异常继续向外抛

关键细节

  • finally 一定执行(进程终止/线程中止等极端情况除外)。
  • 不要在 finally 里再抛异常:会覆盖原始异常,导致根因丢失。
  • 重新抛出用 throw; 而不是 throw ex;,否则会重置堆栈。
示例:finally 覆盖原异常(反例)
try
{throw new InvalidOperationException("业务失败:库存不足");
}
catch (Exception)
{// 记录后准备往外抛throw; // 保留原堆栈
}
finally
{// 千万别这样!这会覆盖上面的异常// throw new Exception("finally 清理失败");
}
示例:确保清理不阻断(每个释放动作单独 try/catch)
finally
{try { CloseFile(); } catch (Exception ex) { _logger.LogError(ex, "关闭文件失败"); }try { CloseDb(); }   catch (Exception ex) { _logger.LogError(ex, "关闭数据库失败"); }try { CloseCache(); }catch (Exception ex) { _logger.LogError(ex, "关闭缓存失败"); }
}
示例:保留多异常信息(必要时聚合)
Exception? origin = null;
try
{throw new Exception("原始异常");
}
catch (Exception ex)
{origin = ex;
}
finally
{try{throw new Exception("finally 内又出错");}catch (Exception ex){if (origin != null) throw new AggregateException(origin, ex);else throw;}
}
示例:异步异常与 AggregateException
// 推荐使用 await(可直接得到原始异常)
await DoAsync();// 若用 .Wait()/Result,异常会包成 AggregateException
try
{DoAsync().Wait();
}
catch (AggregateException ae)
{foreach (var e in ae.Flatten().InnerExceptions)Console.WriteLine(e.Message);
}

using / await using:资源释放的终极武器

using 语法的三种形态

  1. 传统 using 语句(作用域块)
using (var conn = new SqlConnection(cs))
{await conn.OpenAsync();// ...
} // 这里自动调用 conn.Dispose()
  1. using 声明(C# 8+,更简洁)
using var stream = File.OpenRead(path);
// ...
// 作用域结束自动 Dispose()
  1. await using(IAsyncDisposable,C# 8+)
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
// 作用域结束自动调用 fs.DisposeAsync()

正确实现 IDisposable 模式(含非托管资源)

public sealed class SafeNativeHandle : SafeHandle
{public SafeNativeHandle() : base(IntPtr.Zero, true) { }public override bool IsInvalid => handle == IntPtr.Zero;protected override bool ReleaseHandle(){return NativeCloseHandle(handle); // P/Invoke 关闭句柄}
}public class MyResource : IDisposable
{private bool _disposed;private readonly SafeNativeHandle _handle = new();public void Use(){if (_disposed) throw new ObjectDisposedException(nameof(MyResource));// 使用句柄...}public void Dispose(){if (_disposed) return;_handle?.Dispose();_disposed = true;GC.SuppressFinalize(this);}
}

✅ 建议:优先使用 using/await using 管理资源,把“释放失败导致泄漏”的概率打到最低。


ASP.NET Core 中间件拦截:全局异常与日志统一出口

中间件(Middleware)位于最外层,能拦截整个请求管道(静态文件、MVC、SignalR、Minimal API…)。

1)全局异常/日志中间件(生产可用)

RequestLoggingMiddleware.cs

public class RequestLoggingMiddleware
{private readonly RequestDelegate _next;private readonly ILogger<RequestLoggingMiddleware> _logger;public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger){_next = next;_logger = logger;}public async Task InvokeAsync(HttpContext context){var sw = System.Diagnostics.Stopwatch.StartNew();var path = context.Request.Path;var method = context.Request.Method;var traceId = context.TraceIdentifier;try{_logger.LogInformation("REQ {TraceId} {Method} {Path}", traceId, method, path);await _next(context);_logger.LogInformation("RES {TraceId} {StatusCode} in {Elapsed}ms", traceId, context.Response.StatusCode, sw.ElapsedMilliseconds);}catch (Exception ex){_logger.LogError(ex, "UNHANDLED {TraceId} {Method} {Path}", traceId, method, path);context.Response.StatusCode = StatusCodes.Status500InternalServerError;context.Response.ContentType = "application/json";var problem = new ProblemDetails{Title = "服务器开小差了",Status = StatusCodes.Status500InternalServerError,Detail = "请稍后再试或联系管理员",Instance = path};await context.Response.WriteAsJsonAsync(problem);}}
}public static class RequestLoggingMiddlewareExtensions
{public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)=> app.UseMiddleware<RequestLoggingMiddleware>();
}

Program.cs(.NET 8+ 顶级语句)

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();app.UseRequestLogging();              // 一定放在管道靠前位置
app.MapControllers();
app.Run();

2)读取请求体与响应体(可观测增强)

注意:读取请求体需要 EnableBuffering(),读取响应体需要临时替换 Response.Body

public class BodyCaptureMiddleware
{private readonly RequestDelegate _next;private readonly ILogger<BodyCaptureMiddleware> _logger;public BodyCaptureMiddleware(RequestDelegate next, ILogger<BodyCaptureMiddleware> logger){ _next = next; _logger = logger; }public async Task InvokeAsync(HttpContext context){// 请求体context.Request.EnableBuffering();using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true)){var body = await reader.ReadToEndAsync();context.Request.Body.Position = 0; // 归位,交给后续中间件/模型绑定_logger.LogDebug("RequestBody: {Body}", body);}// 响应体var originalBody = context.Response.Body;await using var mem = new MemoryStream();context.Response.Body = mem;await _next(context);mem.Position = 0;var responseText = await new StreamReader(mem).ReadToEndAsync();_logger.LogDebug("ResponseBody: {Body}", responseText);mem.Position = 0;await mem.CopyToAsync(originalBody);context.Response.Body = originalBody;}
}

3)官方内置异常页/处理器(快速集成)

  • 开发环境:app.UseDeveloperExceptionPage();
  • 生产环境:app.UseExceptionHandler("/error"); + 一个 /error 端点统一返回 ProblemDetails

MVC 过滤器拦截:动作/结果/异常的精细化处理

过滤器(Filter)只作用在 MVC 管道 内(Controller/Action),无法拦截 MVC 之外的异常(例如在路由前就抛出)。

1)Action 执行时间与模型验证统一校验(ActionFilter)

public class ValidateAndTimingFilter : IActionFilter
{private readonly ILogger<ValidateAndTimingFilter> _logger;private System.Diagnostics.Stopwatch? _sw;public ValidateAndTimingFilter(ILogger<ValidateAndTimingFilter> logger) => _logger = logger;public void OnActionExecuting(ActionExecutingContext context){_sw = System.Diagnostics.Stopwatch.StartNew();if (!context.ModelState.IsValid){var problem = new ValidationProblemDetails(context.ModelState){Title = "请求参数不合法",Status = StatusCodes.Status400BadRequest};context.Result = new BadRequestObjectResult(problem);}}public void OnActionExecuted(ActionExecutedContext context){_sw?.Stop();_logger.LogInformation("Action {Action} 耗时 {Elapsed}ms",context.ActionDescriptor.DisplayName,_sw?.ElapsedMilliseconds);}
}

注册为全局过滤器

builder.Services.AddControllers(opts =>
{opts.Filters.Add<ValidateAndTimingFilter>();
});

2)统一异常输出(ExceptionFilter)

public class GlobalExceptionFilter : IExceptionFilter
{private readonly ILogger<GlobalExceptionFilter> _logger;private readonly IHostEnvironment _env;public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger, IHostEnvironment env){ _logger = logger; _env = env; }public void OnException(ExceptionContext context){var ex = context.Exception;_logger.LogError(ex, "MVC 未处理异常");var problem = new ProblemDetails{Title = "发生错误",Status = StatusCodes.Status500InternalServerError,Detail = _env.IsDevelopment() ? ex.ToString() : "",Instance = context.HttpContext.Request.Path};context.Result = new ObjectResult(problem){StatusCode = StatusCodes.Status500InternalServerError};context.ExceptionHandled = true; // 防止向外继续抛}
}

注册

builder.Services.AddControllers(opts =>
{opts.Filters.Add<GlobalExceptionFilter>();
});

提示:中间件 vs 过滤器

  • 中间件位于最外层,能兜住所有异常(包括 MVC 前/外)。
  • 异常过滤器专注 MVC 内部(模型绑定/Action/Result),更易做领域化响应转换。
  • 实战推荐:二者结合——中间件统一兜底,过滤器做领域化包装。

3)结果过滤(ResultFilter)——统一包裹响应格式

public class WrapResultFilter : IResultFilter
{public void OnResultExecuting(ResultExecutingContext context){if (context.Result is ObjectResult obj && obj.Value is not ProblemDetails){context.Result = new ObjectResult(new { code = 0, data = obj.Value, msg = "ok" }){StatusCode = obj.StatusCode ?? StatusCodes.Status200OK};}}public void OnResultExecuted(ResultExecutedContext context) { }
}

(可选)Minimal API 的 Endpoint Filters

.NET 7+ 提供 Endpoint Filters,可在 Minimal API 中做拦截。

public class EndpointLogFilter : IEndpointFilter
{private readonly ILogger<EndpointLogFilter> _logger;public EndpointLogFilter(ILogger<EndpointLogFilter> logger) => _logger = logger;public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next){_logger.LogInformation("Endpoint {Route} 调用", context.HttpContext.Request.Path);try{return await next(context);}catch (Exception ex){_logger.LogError(ex, "Endpoint 异常");return Results.Problem(title: "发生错误", statusCode: 500);}}
}var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/ping", () => "pong").AddEndpointFilter<EndpointLogFilter>();
app.Run();

日志与追踪:结构化日志 + 关联ID(CorrelationId)

1)写结构化日志

_logger.LogInformation("订单创建成功:OrderId={OrderId}, User={UserId}", orderId, userId);

2)注入/透传关联 ID

  • 入口生成 Correlation-Id(若客户端未提供),写入 HttpContext.TraceIdentifier 或 Response Header。
  • 所有日志附带该 ID,方便集中检索。

中间件示例

public class CorrelationIdMiddleware
{private const string HeaderName = "X-Correlation-Id";private readonly RequestDelegate _next;public CorrelationIdMiddleware(RequestDelegate next) => _next = next;public async Task InvokeAsync(HttpContext ctx){if (!ctx.Request.Headers.TryGetValue(HeaderName, out var cid) || string.IsNullOrWhiteSpace(cid)){cid = Guid.NewGuid().ToString("N");ctx.Response.Headers[HeaderName] = cid;}using (LogContext.PushProperty("CorrelationId", cid)) // 若使用支持作用域的日志库{await _next(ctx);}}
}

常见反模式与最佳实践清单

反模式

  • finally 里抛新异常,覆盖原异常。
  • 捕获后什么都不做(吞异常)。
  • throw ex; 代替 throw;(破坏堆栈)。
  • 在大量简单分支中用异常控制流程。
  • 不对释放动作分段 try/catch,导致一个资源释放失败“拖死”后续释放。
  • Controller 到处写 try/catch,缺少统一处理(应交给中间件/过滤器)。

最佳实践

  • 就近处理 + 全局兜底:局部业务可恢复异常就地处理,其他交给中间件/过滤器。
  • using/await using 优先,必要时正确实现 IDisposable/IAsyncDisposable
  • 标准化错误响应:使用 ProblemDetails 或统一 {code,msg,data} 契约。
  • 结构化日志 + CorrelationId,便于排查与链路追踪。
  • 异步优先await 可保留原始异常类型/堆栈,避免 AggregateException

完整代码清单与项目骨架

Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{options.Filters.Add<ValidateAndTimingFilter>();options.Filters.Add<GlobalExceptionFilter>();options.Filters.Add<WrapResultFilter>();
});var app = builder.Build();
app.UseRequestLogging();
app.MapControllers();
app.Run();

DemoController.cs

[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{[HttpGet("ok")]public IActionResult OkDemo() => Ok(new { message = "hello" });[HttpGet("boom")]public IActionResult Boom(){using var fs = System.IO.File.OpenRead("/path/not/exist"); // 故意触发异常return Ok();}
}

ValidateAndTimingFilter.cs / GlobalExceptionFilter.cs / WrapResultFilter.cs / RequestLoggingMiddleware.cs

见上文对应小节,直接复制到项目中即可运行。


总结

  • try/catch/finally 解决局部异常与资源释放,但要避开 finally 覆盖异常的坑。
  • using/await using 是释放资源的首选方式。
  • 中间件负责全局兜底一致性(异常与日志),过滤器负责MVC 内部的精细化处理
  • 配合结构化日志与关联 ID,排障提效一个量级。
http://www.dtcms.com/a/328796.html

相关文章:

  • tRAP(tRNA 活性预测器)
  • Java开发主流框架搭配详解及学习路线指南
  • 二叉树的最小深度
  • Android 终端接入 GB28181 国标视频平台的完整解决方案解析
  • 【安卓,问题记录】ImageView 在布局顺序上位于 Button 上方,却出现图像内容被 Button 遮挡
  • AIOPS人才需具备的技术需求
  • 【完整源码+数据集+部署教程】火柴实例分割系统源码和数据集:改进yolo11-rmt
  • latex中“itemize”
  • 如何写出高质量的dify参数提取器prompt
  • 【P21】OpenCV Python——RGB和BGR,HSV和HSL颜色空间,及VScode中报错问题解决
  • vscode扩展应用 -koroFileHeader(jsdoc代码风格注释)
  • .net\c#web、小程序、安卓开发之基于asp.net家用汽车销售管理系统的设计与实现
  • InnoDB如何解决脏读、不可重复读和幻读的?
  • 天文与航天领域专业计算库介绍
  • C# 反射入门:如何获取 Type 对象?
  • Blender模拟结构光3D Scanner(一)外参数匹配
  • 决策树回归:用“分而治之”的智慧,搞定非线性回归难题(附3D可视化)
  • JS 与 C++ 双向通信实战:基于 WebHostViewListener 的消息处理机制
  • Java后端面试题(含Dubbo、MQ、分布式、并发、算法)
  • 分布式与微服务宝典
  • 智能算法流程图在临床工作中的编程视角系统分析
  • 【docker①】在VS Code中使用Docker容器
  • 安全点(Safepoint)完成后唤醒暂停线程的过程
  • 解决uni-app微信小程序编译报错:unexpected character `1`
  • 机器学习实战·第三章 分类(2)
  • EI学术会议 | 虚拟现实、图像和信号处理
  • 股指期货长线还是短线好?
  • AWS Redis Serverless连接完全指南:从安装到实战
  • Notepad++插件开发实战:从入门到精通
  • oss(阿里云)前端直传