博客 / 詳情

返回

C#.NET 全局異常到底怎麼做?最完整的實戰指南

簡介

全局異常攔截是構建健壯企業級應用的關鍵基礎設施,它能統一處理系統中未捕獲的異常,提供友好的錯誤響應,同時記錄完整的異常信息。

背景和作用

ASP.NET Core 應用中,異常可能在控制器、數據庫操作或中間件中發生。如果每個動作方法都手動處理異常(如 try-catch),代碼會變得冗長且難以維護。全局異常攔截器解決了以下問題:

  • 統一錯誤處理:集中捕獲所有未處理異常,返回標準化的錯誤響應。
  • 標準化響應:符合 RESTful API 規範(如 RFC 7807 Problem Details)。
  • 日誌記錄:記錄異常詳情,便於調試和監控。
  • 用户體驗:返回友好的錯誤信息,而非默認錯誤頁面或堆棧跟蹤。
  • 性能優化:減少重複的異常處理代碼,提升開發效率。

主要功能

  • 捕獲異常:捕獲控制器、服務層或其他代碼中的未處理異常。
  • 標準化響應:返回 JSON 格式的錯誤詳情(如狀態碼、錯誤消息)。
  • 日誌記錄:記錄異常信息(包括堆棧跟蹤)到日誌系統。
  • 自定義處理:根據異常類型返回不同狀態碼或消息(如 400、409、500)。
  • 異步支持:兼容 async/await,適合異步操作。
  • DI 集成:通過依賴注入訪問服務(如日誌、緩存)。

常見實現方式

異常處理中間件(推薦)

ASP.NET Core 管道最前端註冊一箇中間件,捕獲所有後續中間件/終結點拋出的異常。

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        try
        {
            await _next(ctx);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");
            await HandleExceptionAsync(ctx, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext ctx, Exception ex)
    {
        ctx.Response.ContentType = "application/problem+json";
        ctx.Response.StatusCode = ex switch
        {
            ArgumentException _ => StatusCodes.Status400BadRequest,
            KeyNotFoundException _ => StatusCodes.Status404NotFound,
            _ => StatusCodes.Status500InternalServerError
        };

        var problem = new ProblemDetails
        {
            Title = ex.Message,
            Status = ctx.Response.StatusCode,
            Detail  = ctx.Response.StatusCode == 500 ? "請稍後重試或聯繫管理員" : null,
            Instance = ctx.Request.Path
        };
        var json = JsonSerializer.Serialize(problem);
        return ctx.Response.WriteAsync(json);
    }
}

註冊中間件

Program.cs(或 Startup.cs)中最早添加:

app.UseMiddleware<ExceptionHandlingMiddleware>();

// 或更簡潔的擴展方法
app.UseExceptionHandling();  // 需自行實現 UseExceptionHandling 擴展

優勢

  • 捕獲範圍最大:包括所有 MVC、Minimal API、靜態文件等。
  • 性能開銷小,代碼集中清晰。
  • 易於與依賴注入、日誌系統聯動。

全局異常過濾器(IExceptionFilter / IAsyncExceptionFilter)

如果只想攔截 MVC Controller 的異常,可實現異常過濾器。

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;
    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
        => _logger = logger;

    public void OnException(ExceptionContext context)
    {
        var ex = context.Exception;
        _logger.LogError(ex, "Unhandled exception in controller");

        var problem = new ProblemDetails
        {
            Title = "請求失敗",
            Status = StatusCodes.Status500InternalServerError,
            Detail = ex.Message
        };
        context.Result = new ObjectResult(problem)
        {
            StatusCode = problem.Status
        };
        context.ExceptionHandled = true;
    }
}

註冊過濾器

services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

侷限

  • 只攔截通過 MVC 管道執行的 Action 拋出的異常。
  • 不會捕獲例如中間件或 Minimal API 的異常。

.NET 7+ 新增的 IExceptionHandler(推薦)

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    private readonly IProblemDetailsService _problemDetailsService;

    public GlobalExceptionHandler(
        ILogger<GlobalExceptionHandler> logger,
        IProblemDetailsService problemDetailsService)
    {
        _logger = logger;
        _problemDetailsService = problemDetailsService;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "全局異常: {Message}", exception.Message);
        
        var statusCode = GetStatusCode(exception);
        var problemContext = new ProblemDetailsContext
        {
            HttpContext = httpContext,
            Exception = exception,
            ProblemDetails = new ProblemDetails
            {
                Title = "服務器錯誤",
                Status = statusCode,
                Detail = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment() 
                    ? exception.ToString() 
                    : "請稍後再試",
                Type = $"https://httpstatuses.io/{statusCode}"
            }
        };
        
        // 添加自定義擴展
        problemContext.ProblemDetails.Extensions.Add("requestId", httpContext.TraceIdentifier);
        
        await _problemDetailsService.WriteAsync(problemContext);
        return true; // 標記為已處理
    }
}

註冊服務:

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails(); // 添加ProblemDetails支持

var app = builder.Build();
app.UseExceptionHandler(); // 啓用異常處理中間件

內置 UseExceptionHandler

ASP.NET Core 自帶的異常處理終結點:

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async ctx =>
        {
            var feature = ctx.Features.Get<IExceptionHandlerFeature>();
            var ex = feature?.Error;
            // 記錄日誌…
            ctx.Response.StatusCode = 500;
            await ctx.Response.WriteAsJsonAsync(new { message = "服務器內部錯誤" });
        });
    });
}
else
{
    app.UseDeveloperExceptionPage();
}
  • 優點:無需自定義 Middleware,框架內置。
  • 注意:要在其他中間件之前註冊,並在生產/開發環境中分開處理。

資源和文檔

  • 官方文檔:

    • Exception Handling:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/er...
    • Problem Details:https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-...
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.