C# 异常处理与拦截全攻略:try/catch/finally、using、ASP.NET Core 中间件与过滤器一网打尽(含完整示例)
面向:.NET 后端/全栈工程师、API 开发者、需要做统一异常处理与日志追踪的同学。 亮点:系统化讲清楚 try/catch/finally 的执行细节与坑、using/await using 的正确姿势、ASP.NET Core 中间件与过滤器的“全链路拦截”,送上可复制落地代码与最佳实践清单。
目录
异常模型与基本原则
.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 (var conn = new SqlConnection(cs))
{
await conn.OpenAsync();
// …
} // 这里自动调用 conn.Dispose()
using var stream = File.OpenRead(path);
// …
// 作用域结束自动 Dispose()
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,排障提效一个量级。
评论前必须登录!
注册