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

发布于:2025-08-14 ⋅ 阅读:(28) ⋅ 点赞:(0)

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,排障提效一个量级。

网站公告

今日签到

点亮在社区的每一天
去签到