云计算百科
云计算领域专业知识百科平台

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,排障提效一个量级。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C# 异常处理与拦截全攻略:try/catch/finally、using、ASP.NET Core 中间件与过滤器一网打尽(含完整示例)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!