全局异常处理对于创建健壮、安全且可维护的.NET应用程序至关重要。.NET 9提供了多种复杂的方法来优雅地处理异常,从传统的自定义中间件到现代的IExceptionHandler接口。本综合指南探讨了提供一致错误响应、适当日志记录和增强安全性的优雅解决方案。
.NET中错误处理的演进
传统挑战
在适当的全局错误处理之前:
.NET 9中的现代解决方案
.NET 9提供三种主要方法:
方法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 9中的优雅全局异常处理将错误管理从分散的try-catch块转变为有凝聚力、可维护的系统:
提供的关键优势: 🛡️ 增强安全性:防止敏感信息泄露,同时提供有意义的错误响应
📊 结构化响应:符合RFC 7807的问题详情,实现一致的API行为
🔍 更好的可观察性:包含关联ID和结构化数据的全面日志记录
⚡ 改进的性能:集中处理减少代码重复并提高可维护性
🧪 增强的可测试性:可以独立进行单元测试的专注异常处理器
👥 开发人员体验:清晰、一致的错误响应,帮助开发人员和消费者
实施策略:
何时使用每种方法: IExceptionHandler(推荐用于.NET 8+):
自定义中间件:
现代IExceptionHandler方法在功能性、可维护性和性能之间提供了最佳平衡。它与ASP.NET Core的管道无缝集成,同时提供了适当处理不同异常类型的灵活性。结合RFC 7807问题详情和结构化日志记录,它创建了一个健壮的错误处理基础,可以随着应用程序复杂性的增长而扩展。