.NET 9全局异常处理终极指南:从传统中间件到现代IExceptionHandler的优雅演进

作者:微信公众号:【架构师老卢】
9-23 13:56
6

全局异常处理对于创建健壮、安全且可维护的.NET应用程序至关重要。.NET 9提供了多种复杂的方法来优雅地处理异常,从传统的自定义中间件到现代的IExceptionHandler接口。本综合指南探讨了提供一致错误响应、适当日志记录和增强安全性的优雅解决方案。

.NET中错误处理的演进
传统挑战
在适当的全局错误处理之前:

  • 暴露给客户端的堆栈跟踪揭示了敏感的应用程序细节
  • 跨端点的错误响应格式不一致
  • 控制器中散布的try-catch块
  • 日志记录和监控能力差
  • 通过信息泄露导致的安全漏洞

.NET 9中的现代解决方案
.NET 9提供三种主要方法:

  • 自定义异常处理中间件(经典、灵活的方法)
  • IExceptionHandler接口(现代,推荐用于.NET 8+)
  • 内置UseExceptionHandler(简单、快速设置)

方法1:自定义异常处理中间件
实现
创建健壮的自定义中间件以完全控制错误处理:

// Middleware/GlobalExceptionHandlingMiddleware.cs
public class GlobalExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;
    private readonly IWebHostEnvironment _environment;

    public GlobalExceptionHandlingMiddleware(
        RequestDelegate next,
        ILogger<GlobalExceptionHandlingMiddleware> logger,
        IWebHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "发生未处理异常。请求ID: {RequestId}, 路径: {Path}", 
                context.TraceIdentifier, context.Request.Path);
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/problem+json";
        var problemDetails = CreateProblemDetails(context, exception);
        
        context.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        var json = JsonSerializer.Serialize(problemDetails, options);
        await context.Response.WriteAsync(json);
    }

    private ProblemDetails CreateProblemDetails(HttpContext context, Exception exception)
    {
        var (statusCode, title, detail) = MapException(exception);
        return new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = _environment.IsDevelopment() ? exception.Message : detail,
            Instance = context.Request.Path,
            Type = $"https://httpstatuses.com/{statusCode}",
            Extensions = new Dictionary<string, object?>
            {
                ["traceId"] = context.TraceIdentifier,
                ["timestamp"] = DateTime.UtcNow,
                ["exception"] = _environment.IsDevelopment() ? exception.GetType().Name : null
            }
        };
    }

    private static (int statusCode, string title, string detail) MapException(Exception exception)
    {
        return exception switch
        {
            ValidationException => (StatusCodes.Status400BadRequest, 
                "验证错误", "发生一个或多个验证错误。"),
            
            UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, 
                "未授权", "需要身份验证才能访问此资源。"),
            
            ForbiddenException => (StatusCodes.Status403Forbidden, 
                "禁止访问", "您没有权限访问此资源。"),
            
            NotFoundException => (StatusCodes.Status404NotFound, 
                "资源未找到", "请求的资源未找到。"),
            
            ConflictException => (StatusCodes.Status409Conflict, 
                "冲突", "由于冲突无法完成请求。"),
            
            ArgumentException => (StatusCodes.Status400BadRequest, 
                "错误请求", "请求包含无效参数。"),
            
            InvalidOperationException => (StatusCodes.Status400BadRequest, 
                "无效操作", "该操作对当前状态无效。"),
            
            TimeoutException => (StatusCodes.Status408RequestTimeout, 
                "请求超时", "请求超时。"),
            
            _ => (StatusCodes.Status500InternalServerError, 
                "内部服务器错误", "发生意外错误。")
        };
    }
}

自定义异常类
定义领域特定异常以更好地映射错误:

// Exceptions/BaseException.cs
public abstract class BaseException : Exception
{
    protected BaseException(string message) : base(message) { }
    protected BaseException(string message, Exception innerException) : base(message, innerException) { }
    
    public abstract int StatusCode { get; }
    public abstract string ErrorType { get; }
}

// Exceptions/ValidationException.cs
public class ValidationException : BaseException
{
    public ValidationException(string message) : base(message) { }
    public ValidationException(string message, Exception innerException) : base(message, innerException) { }
    
    public ValidationException(IDictionary<string, string[]> errors) 
        : base("发生一个或多个验证错误。")
    {
        Errors = errors;
    }

    public override int StatusCode => StatusCodes.Status400BadRequest;
    public override string ErrorType => "ValidationError";
    public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>();
}

// Exceptions/NotFoundException.cs
public class NotFoundException : BaseException
{
    public NotFoundException(string message) : base(message) { }
    public NotFoundException(string name, object key) 
        : base($"实体 '{name}' 与键 '{key}' 未找到。") { }

    public override int StatusCode => StatusCodes.Status404NotFound;
    public override string ErrorType => "NotFound";
}

// Exceptions/ConflictException.cs
public class ConflictException : BaseException
{
    public ConflictException(string message) : base(message) { }
    public override int StatusCode => StatusCodes.Status409Conflict;
    public override string ErrorType => "Conflict";
}

注册
在管道中注册中间件:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 添加服务
builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

// 添加全局异常处理中间件(应在管道早期)
app.UseMiddleware<GlobalExceptionHandlingMiddleware>();

// 其他中间件
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

方法2:现代IExceptionHandler(推荐)
实现
使用.NET 8引入的现代IExceptionHandler接口:

// Handlers/GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    private readonly IWebHostEnvironment _environment;
    private readonly IProblemDetailsService _problemDetailsService;

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

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // 使用结构化日志记录异常
        _logger.LogError(exception, 
            "发生异常: {Message} | 请求ID: {RequestId} | 路径: {Path} | 方法: {Method}",
            exception.Message, 
            httpContext.TraceIdentifier, 
            httpContext.Request.Path.Value, 
            httpContext.Request.Method);

        var problemDetails = CreateProblemDetails(httpContext, exception);
        
        httpContext.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;

        // 使用内置问题详情服务
        return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails = problemDetails,
            Exception = exception
        });
    }

    private ProblemDetails CreateProblemDetails(HttpContext httpContext, Exception exception)
    {
        var (statusCode, title, detail, errorType) = MapException(exception);
        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = _environment.IsDevelopment() ? exception.Message : detail,
            Type = errorType,
            Instance = httpContext.Request.Path
        };

        // 添加自定义扩展
        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;
        problemDetails.Extensions["timestamp"] = DateTime.UtcNow;
        problemDetails.Extensions["requestId"] = httpContext.TraceIdentifier;

        // 添加仅开发环境信息
        if (_environment.IsDevelopment())
        {
            problemDetails.Extensions["exceptionType"] = exception.GetType().Name;
            problemDetails.Extensions["stackTrace"] = exception.StackTrace;
        }

        // 特殊处理验证异常
        if (exception is ValidationException validationEx)
        {
            problemDetails.Extensions["errors"] = validationEx.Errors;
        }

        return problemDetails;
    }

    private static (int statusCode, string title, string detail, string errorType) MapException(Exception exception)
    {
        return exception switch
        {
            ValidationException => (StatusCodes.Status400BadRequest,
                "验证错误",
                "发生一个或多个验证错误。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.1"),

            UnauthorizedAccessException => (StatusCodes.Status401Unauthorized,
                "未授权",
                "需要身份验证才能访问此资源。",
                "https://tools.ietf.org/html/rfc7235#section-3.1"),

            ForbiddenException => (StatusCodes.Status403Forbidden,
                "禁止访问",
                "您没有权限访问此资源。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.3"),

            NotFoundException => (StatusCodes.Status404NotFound,
                "资源未找到",
                "请求的资源未找到。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.4"),

            ConflictException => (StatusCodes.Status409Conflict,
                "冲突",
                "由于冲突无法完成请求。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.8"),

            ArgumentException => (StatusCodes.Status400BadRequest,
                "错误请求",
                "请求包含无效参数。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.1"),

            InvalidOperationException => (StatusCodes.Status400BadRequest,
                "无效操作",
                "该操作对当前状态无效。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.1"),

            TaskCanceledException => (StatusCodes.Status408RequestTimeout,
                "请求超时",
                "请求超时。",
                "https://tools.ietf.org/html/rfc7231#section-6.5.7"),

            _ => (StatusCodes.Status500InternalServerError,
                "内部服务器错误",
                "发生意外错误,请稍后重试。",
                "https://tools.ietf.org/html/rfc7231#section-6.6.1")
        };
    }
}

专用异常处理器
为特定异常类型创建专注的处理器:

// Handlers/ValidationExceptionHandler.cs
public class ValidationExceptionHandler : IExceptionHandler
{
    private readonly ILogger<ValidationExceptionHandler> _logger;

    public ValidationExceptionHandler(ILogger<ValidationExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // 仅处理ValidationException
        if (exception is not ValidationException validationException)
            return false;

        _logger.LogWarning("验证失败: {Message} | 请求ID: {RequestId}", 
            validationException.Message, httpContext.TraceIdentifier);

        var problemDetails = new ValidationProblemDetails(validationException.Errors)
        {
            Status = StatusCodes.Status400BadRequest,
            Title = "验证错误",
            Detail = "发生一个或多个验证错误。",
            Instance = httpContext.Request.Path,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
        };

        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;
        problemDetails.Extensions["timestamp"] = DateTime.UtcNow;

        httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // 异常已处理
    }
}

// Handlers/NotFoundExceptionHandler.cs
public class NotFoundExceptionHandler : IExceptionHandler
{
    private readonly ILogger<NotFoundExceptionHandler> _logger;

    public NotFoundExceptionHandler(ILogger<NotFoundExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFoundException)
            return false;

        _logger.LogWarning("资源未找到: {Message} | 请求ID: {RequestId}", 
            notFoundException.Message, httpContext.TraceIdentifier);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "资源未找到",
            Detail = notFoundException.Message,
            Instance = httpContext.Request.Path,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"
        };

        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;
        problemDetails.Extensions["timestamp"] = DateTime.UtcNow;

        httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

注册和配置
以适当的顺序注册异常处理器:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 添加服务
builder.Services.AddControllers();

// 添加问题详情服务
builder.Services.AddProblemDetails(options =>
{
    // 全局自定义问题详情
    options.CustomizeProblemDetails = (context) =>
    {
        context.ProblemDetails.Extensions["machine"] = Environment.MachineName;
        context.ProblemDetails.Extensions["requestId"] = context.HttpContext.TraceIdentifier;
        
        // 添加关联ID(如果可用)
        if (context.HttpContext.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
        {
            context.ProblemDetails.Extensions["correlationId"] = correlationId.ToString();
        }
    };
});

// 按特异性顺序注册异常处理器(最具体的先注册)
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // 后备处理器

var app = builder.Build();

// 配置管道
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    // 使用内置异常处理器中间件
    app.UseExceptionHandler();
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

RFC 7807问题详情实现
增强的问题详情
实现标准化的RFC 7807问题详情:

// Models/ApiProblemDetails.cs
public class ApiProblemDetails : ProblemDetails
{
    public string? TraceId { get; set; }
    public DateTime Timestamp { get; set; }
    public string? CorrelationId { get; set; }
    public Dictionary<string, object>? Context { get; set; }
    public List<ApiError>? Errors { get; set; }
}

public class ApiError
{
    public string Code { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string? Field { get; set; }
    public object? Value { get; set; }
}

// Services/ProblemDetailsFactory.cs
public class ProblemDetailsFactory
{
    public static ApiProblemDetails CreateProblemDetails(
        HttpContext context, 
        Exception exception, 
        bool isDevelopment = false)
    {
        var (statusCode, title, detail, type) = MapException(exception);
        
        var problemDetails = new ApiProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = isDevelopment ? exception.Message : detail,
            Type = type,
            Instance = context.Request.Path.Value,
            TraceId = context.TraceIdentifier,
            Timestamp = DateTime.UtcNow,
            CorrelationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
        };

        // 添加上下文信息
        problemDetails.Context = new Dictionary<string, object>
        {
            ["method"] = context.Request.Method,
            ["path"] = context.Request.Path.Value ?? string.Empty,
            ["queryString"] = context.Request.QueryString.Value ?? string.Empty,
            ["userAgent"] = context.Request.Headers["User-Agent"].FirstOrDefault() ?? string.Empty
        };

        // 处理验证异常
        if (exception is ValidationException validationEx)
        {
            problemDetails.Errors = validationEx.Errors.SelectMany(kvp =>
                kvp.Value.Select(error => new ApiError
                {
                    Code = "VALIDATION_ERROR",
                    Description = error,
                    Field = kvp.Key
                })).ToList();
        }

        // 添加开发环境信息
        if (isDevelopment)
        {
            problemDetails.Context["exceptionType"] = exception.GetType().FullName ?? string.Empty;
            problemDetails.Context["stackTrace"] = exception.StackTrace ?? string.Empty;
        }

        return problemDetails;
    }

    private static (int statusCode, string title, string detail, string type) MapException(Exception exception)
    {
        return exception switch
        {
            ValidationException => (400, "验证错误", 
                "发生一个或多个验证错误。", 
                "https://httpstatuses.com/400"),
                
            NotFoundException => (404, "资源未找到", 
                "请求的资源未找到。", 
                "https://httpstatuses.com/404"),
                
            UnauthorizedAccessException => (401, "未授权", 
                "需要身份验证才能访问此资源。", 
                "https://httpstatuses.com/401"),
                
            ForbiddenException => (403, "禁止访问", 
                "您没有权限访问此资源。", 
                "https://httpstatuses.com/403"),
                
            ConflictException => (409, "冲突", 
                "由于冲突无法完成请求。", 
                "https://httpstatuses.com/409"),
                
            _ => (500, "内部服务器错误", 
                "发生意外错误。", 
                "https://httpstatuses.com/500")
        };
    }
}

结构化日志记录集成
增强的日志记录
实现包含结构化数据的全面日志记录:

// Services/StructuredLoggingExceptionHandler.cs
public class StructuredLoggingExceptionHandler : IExceptionHandler
{
    private readonly ILogger<StructuredLoggingExceptionHandler> _logger;
    private readonly IWebHostEnvironment _environment;
    private readonly IProblemDetailsService _problemDetailsService;

    public StructuredLoggingExceptionHandler(
        ILogger<StructuredLoggingExceptionHandler> logger,
        IWebHostEnvironment environment,
        IProblemDetailsService problemDetailsService)
    {
        _logger = logger;
        _environment = environment;
        _problemDetailsService = problemDetailsService;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // 创建结构化日志数据
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = httpContext.TraceIdentifier,
            ["RequestPath"] = httpContext.Request.Path.Value ?? string.Empty,
            ["RequestMethod"] = httpContext.Request.Method,
            ["UserAgent"] = httpContext.Request.Headers["User-Agent"].FirstOrDefault() ?? string.Empty,
            ["RemoteIpAddress"] = httpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown",
            ["UserId"] = httpContext.User?.Identity?.Name ?? "Anonymous",
            ["CorrelationId"] = httpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault()
        });

        // 根据异常严重性记录日志
        var logLevel = DetermineLogLevel(exception);
        _logger.Log(logLevel, exception, 
            "发生未处理异常: {ExceptionType} - {ExceptionMessage}",
            exception.GetType().Name, exception.Message);

        // 额外的指标日志记录
        LogExceptionMetrics(httpContext, exception);

        var problemDetails = ProblemDetailsFactory.CreateProblemDetails(
            httpContext, exception, _environment.IsDevelopment());

        httpContext.Response.StatusCode = problemDetails.Status ?? 500;

        return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails = problemDetails,
            Exception = exception
        });
    }

    private static LogLevel DetermineLogLevel(Exception exception)
    {
        return exception switch
        {
            ValidationException => LogLevel.Warning,
            NotFoundException => LogLevel.Warning,
            UnauthorizedAccessException => LogLevel.Warning,
            ForbiddenException => LogLevel.Warning,
            ConflictException => LogLevel.Information,
            ArgumentException => LogLevel.Warning,
            _ => LogLevel.Error
        };
    }

    private void LogExceptionMetrics(HttpContext httpContext, Exception exception)
    {
        // 记录监控的额外指标
        _logger.LogInformation("异常指标: {ExceptionType} | 状态码: {StatusCode} | 持续时间: {Duration}ms",
            exception.GetType().Name,
            GetStatusCodeFromException(exception),
            GetRequestDuration(httpContext));
    }

    private static int GetStatusCodeFromException(Exception exception)
    {
        return exception switch
        {
            ValidationException => 400,
            NotFoundException => 404,
            UnauthorizedAccessException => 401,
            ForbiddenException => 403,
            ConflictException => 409,
            _ => 500
        };
    }

    private static long GetRequestDuration(HttpContext httpContext)
    {
        if (httpContext.Items.TryGetValue("RequestStartTime", out var startTimeObj) 
            && startTimeObj is DateTime startTime)
        {
            return (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
        }
        return 0;
    }
}

性能和安全性考虑
性能优化
实现高效的异常处理模式:

// Middleware/RequestTimingMiddleware.cs
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;

    public RequestTimingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        context.Items["RequestStartTime"] = DateTime.UtcNow;
        await _next(context);
    }
}

// Services/CachedExceptionHandler.cs
public class CachedExceptionHandler : IExceptionHandler
{
    private readonly IMemoryCache _cache;
    private readonly ILogger<CachedExceptionHandler> _logger;
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var cacheKey = $"error_response_{exception.GetType().Name}_{httpContext.Request.Path}";
        
        if (!_cache.TryGetValue(cacheKey, out ProblemDetails? cachedProblemDetails))
        {
            cachedProblemDetails = CreateProblemDetails(httpContext, exception);
            _cache.Set(cacheKey, cachedProblemDetails, CacheDuration);
        }

        httpContext.Response.StatusCode = cachedProblemDetails.Status ?? 500;
        await httpContext.Response.WriteAsJsonAsync(cachedProblemDetails, cancellationToken);
        
        return true;
    }

    private ProblemDetails CreateProblemDetails(HttpContext httpContext, Exception exception)
    {
        // 实现代码...
        return new ProblemDetails();
    }
}

安全性增强
实现注重安全的错误处理:

// Handlers/SecurityAwareExceptionHandler.cs
public class SecurityAwareExceptionHandler : IExceptionHandler
{
    private readonly ILogger<SecurityAwareExceptionHandler> _logger;
    private readonly IWebHostEnvironment _environment;
    private readonly SecuritySettings _securitySettings;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // 记录安全相关异常信息
        if (IsSecurityException(exception))
        {
            _logger.LogWarning("安全异常: {ExceptionType} | IP: {RemoteIp} | 用户: {User} | 路径: {Path}",
                exception.GetType().Name,
                httpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown",
                httpContext.User?.Identity?.Name ?? "Anonymous",
                httpContext.Request.Path.Value);
        }

        var problemDetails = CreateSecureProblemDetails(httpContext, exception);
        
        // 添加安全头
        httpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
        httpContext.Response.Headers.Add("X-Frame-Options", "DENY");
        
        httpContext.Response.StatusCode = problemDetails.Status ?? 500;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        
        return true;
    }

    private bool IsSecurityException(Exception exception)
    {
        return exception is UnauthorizedAccessException 
            or SecurityException 
            or ForbiddenException;
    }

    private ProblemDetails CreateSecureProblemDetails(HttpContext httpContext, Exception exception)
    {
        var (statusCode, title, detail) = GetSecureErrorInfo(exception);
        
        return new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = _environment.IsDevelopment() ? exception.Message : detail,
            Instance = httpContext.Request.Path,
            Extensions = new Dictionary<string, object?>
            {
                ["timestamp"] = DateTime.UtcNow,
                ["requestId"] = httpContext.TraceIdentifier
                // 生产环境中不包含敏感调试信息
            }
        };
    }

    private static (int statusCode, string title, string detail) GetSecureErrorInfo(Exception exception)
    {
        return exception switch
        {
            UnauthorizedAccessException => (401, "未授权", "需要身份验证"),
            ForbiddenException => (403, "禁止访问", "访问被拒绝"),
            SecurityException => (403, "安全错误", "违反安全策略"),
            _ => (500, "内部服务器错误", "发生错误")
        };
    }
}

测试异常处理
单元测试
全面测试你的异常处理器:

// Tests/GlobalExceptionHandlerTests.cs
public class GlobalExceptionHandlerTests
{
    private readonly GlobalExceptionHandler _handler;
    private readonly Mock<ILogger<GlobalExceptionHandler>> _loggerMock;
    private readonly Mock<IProblemDetailsService> _problemDetailsServiceMock;
    private readonly DefaultHttpContext _httpContext;

    public GlobalExceptionHandlerTests()
    {
        _loggerMock = new Mock<ILogger<GlobalExceptionHandler>>();
        _problemDetailsServiceMock = new Mock<IProblemDetailsService>();
        var environment = new Mock<IWebHostEnvironment>();
        environment.Setup(e => e.EnvironmentName).Returns("Development");

        _handler = new GlobalExceptionHandler(
            _loggerMock.Object, 
            environment.Object, 
            _problemDetailsServiceMock.Object);

        _httpContext = new DefaultHttpContext();
        _httpContext.TraceIdentifier = "test-trace-id";
        _httpContext.Request.Path = "/test";
        _httpContext.Request.Method = "GET";
    }

    [Fact]
    public async Task TryHandleAsync_WithValidationException_ReturnsTrue()
    {
        // 准备
        var exception = new ValidationException("测试验证错误");
        _problemDetailsServiceMock
            .Setup(x => x.TryWriteAsync(It.IsAny<ProblemDetailsContext>()))
            .ReturnsAsync(true);

        // 执行
        var result = await _handler.TryHandleAsync(_httpContext, exception, CancellationToken.None);

        // 断言
        Assert.True(result);
        Assert.Equal(400, _httpContext.Response.StatusCode);
        
        _loggerMock.Verify(
            x => x.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("测试验证错误")),
                exception,
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }

    [Fact]
    public async Task TryHandleAsync_WithNotFoundException_Returns404()
    {
        // 准备
        var exception = new NotFoundException("User", "123");
        _problemDetailsServiceMock
            .Setup(x => x.TryWriteAsync(It.IsAny<ProblemDetailsContext>()))
            .ReturnsAsync(true);

        // 执行
        var result = await _handler.TryHandleAsync(_httpContext, exception, CancellationToken.None);

        // 断言
        Assert.True(result);
        Assert.Equal(404, _httpContext.Response.StatusCode);
    }
}

集成测试
测试完整的错误处理管道:

// Tests/ErrorHandlingIntegrationTests.cs
public class ErrorHandlingIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public ErrorHandlingIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetNonExistentResource_Returns404WithProblemDetails()
    {
        // 执行
        var response = await _client.GetAsync("/api/users/999999");

        // 断言
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
        Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType);

        var content = await response.Content.ReadAsStringAsync();
        var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(content, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        Assert.NotNull(problemDetails);
        Assert.Equal(404, problemDetails.Status);
        Assert.Equal("资源未找到", problemDetails.Title);
        Assert.NotNull(problemDetails.Instance);
    }

    [Fact]
    public async Task PostInvalidData_Returns400WithValidationErrors()
    {
        // 准备
        var invalidUser = new { Name = "", Email = "invalid-email" };
        var json = JsonSerializer.Serialize(invalidUser);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        // 执行
        var response = await _client.PostAsync("/api/users", content);

        // 断言
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

        var responseContent = await response.Content.ReadAsStringAsync();
        var problemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(responseContent, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        Assert.NotNull(problemDetails);
        Assert.Equal(400, problemDetails.Status);
        Assert.NotEmpty(problemDetails.Errors);
    }
}

最佳实践和指南
实施指南
✅ 应该做:

  • 对.NET 8+使用IExceptionHandler — 这是现代推荐的方法
  • 使用关联ID实现结构化日志记录以更好地进行故障排除
  • 遵循RFC 7807实现标准化问题详情响应
  • 按特异性处理异常 — 特定处理器优先于通用处理器
  • 在生产环境中清理错误消息以避免信息泄露
  • 包含跟踪ID以将日志与响应关联

❌ 不应该做:

  • 在生产环境中暴露堆栈跟踪
  • 在异常消息中记录敏感信息
  • 使用通用异常处理器进行业务逻辑验证
  • 忽略复杂错误处理的性能影响
  • 在异常处理器中混合身份验证/授权逻辑

安全性考虑
保护敏感信息:

  • 切勿在错误消息中暴露内部系统详情
  • 对开发和生产环境使用不同的错误消息
  • 对重复错误请求实施速率限制
  • 记录安全相关异常并添加上下文
  • 在包含在错误响应之前清理用户输入

性能影响
最小化性能开销:

  • 适当缓存频繁发生的错误响应
  • 高效使用结构化日志记录
  • 避免在异常处理器中进行昂贵操作
  • 考虑处理程序中I/O操作的异步模式
  • 监控和测量异常处理性能

结论
.NET 9中的优雅全局异常处理将错误管理从分散的try-catch块转变为有凝聚力、可维护的系统:

提供的关键优势: 🛡️ 增强安全性:防止敏感信息泄露,同时提供有意义的错误响应

📊 结构化响应:符合RFC 7807的问题详情,实现一致的API行为

🔍 更好的可观察性:包含关联ID和结构化数据的全面日志记录

⚡ 改进的性能:集中处理减少代码重复并提高可维护性

🧪 增强的可测试性:可以独立进行单元测试的专注异常处理器

👥 开发人员体验:清晰、一致的错误响应,帮助开发人员和消费者

实施策略:

  • 选择正确方法:对.NET 8+项目使用IExceptionHandler,对旧版本使用自定义中间件
  • 实现RFC 7807:使用标准化问题详情实现一致的错误响应
  • 添加结构化日志记录:包含关联ID和上下文信息
  • 分层异常处理器:在通用异常之前处理特定异常
  • 安全第一:切勿在生产错误响应中暴露敏感信息
  • 全面测试:为错误处理场景编写单元和集成测试

何时使用每种方法: IExceptionHandler(推荐用于.NET 8+):

  • 需要复杂错误处理的现代应用程序
  • 需要不同处理逻辑的多种异常类型
  • 优先考虑清晰架构和关注点分离的应用程序
  • 需要全面日志记录和监控的系统

自定义中间件:

  • .NET 6/7应用程序或需要最大控制权的情况
  • 具有基本错误处理要求的简单应用程序
  • 需要逐步现代化的遗留系统

现代IExceptionHandler方法在功能性、可维护性和性能之间提供了最佳平衡。它与ASP.NET Core的管道无缝集成,同时提供了适当处理不同异常类型的灵活性。结合RFC 7807问题详情和结构化日志记录,它创建了一个健壮的错误处理基础,可以随着应用程序复杂性的增长而扩展。

相关留言评论
昵称:
邮箱:
阅读排行