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 中间件与过滤器的“全链路拦截”,送上可复制落地代码与最佳实践清单。
目录
- 异常模型与基本原则
- try/catch/finally:执行顺序、隐藏陷阱与正确用法
- using / await using:资源释放的终极武器
- ASP.NET Core 中间件拦截:全局异常与日志统一出口
- MVC 过滤器拦截:动作/结果/异常的精细化处理
- (可选)Minimal API 的 Endpoint Filters
- 日志与追踪:结构化日志 + 关联ID(CorrelationId)
- 常见反模式与最佳实践清单
- 完整代码清单与项目骨架
异常模型与基本原则
.NET 中异常的本质
- 异常(Exception)是不可预期或不应在正常流程中出现的错误。
- 异常是栈展开(stack unwinding)过程中被抛出并一路向外传播,直到被某个
catch
捕获;若没人捕获,进程/请求终结。
基本原则
- 就近处理、集中兜底:业务层就近处理明确可恢复的异常;框架层用中间件做全局兜底。
- 不要吞异常:捕获后必须记录、转换或重抛,避免“悄无声息”。
- 抛出语义化异常:用自定义业务异常或标准异常族,携带上下文信息。
- 不要把异常当分支控制:异常只用于异常路径。
try/catch/finally:执行顺序、隐藏陷阱与正确用法
执行顺序
- 正常:
try
→finally
- 有异常并被捕获:
try
→catch
→finally
- 有异常但未被捕获:
try
→finally
→ 异常继续向外抛
关键细节
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 语法的三种形态
- 传统 using 语句(作用域块)
using (var conn = new SqlConnection(cs))
{await conn.OpenAsync();// ...
} // 这里自动调用 conn.Dispose()
- using 声明(C# 8+,更简洁)
using var stream = File.OpenRead(path);
// ...
// 作用域结束自动 Dispose()
- 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,排障提效一个量级。