Skip to main content

全局异常处理

异常处理中间件

基本实现

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 context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}

private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";

var response = exception switch
{
NotFoundException => new ErrorResponse
{
StatusCode = StatusCodes.Status404NotFound,
Message = exception.Message
},
ValidationException => new ErrorResponse
{
StatusCode = StatusCodes.Status400BadRequest,
Message = exception.Message,
Errors = ((ValidationException)exception).Errors
},
UnauthorizedException => new ErrorResponse
{
StatusCode = StatusCodes.Status401Unauthorized,
Message = exception.Message
},
ForbiddenException => new ErrorResponse
{
StatusCode = StatusCodes.Status403Forbidden,
Message = exception.Message
},
_ => new ErrorResponse
{
StatusCode = StatusCodes.Status500InternalServerError,
Message = "An error occurred while processing your request"
}
};

context.Response.StatusCode = response.StatusCode;
await context.Response.WriteAsJsonAsync(response);
}
}

// 注册中间件
app.UseMiddleware<ExceptionHandlingMiddleware>();

错误响应模型

public class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; } = string.Empty;
public Dictionary<string, string[]>? Errors { get; set; }
public string? StackTrace { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

自定义异常

// 基础异常类
public abstract class ApplicationException : Exception
{
protected ApplicationException(string message) : base(message)
{
}
}

// 404 异常
public class NotFoundException : ApplicationException
{
public NotFoundException(string name, object key)
: base($"{name} with id ({key}) was not found")
{
}

public NotFoundException(string message) : base(message)
{
}
}

// 400 验证异常
public class ValidationException : ApplicationException
{
public Dictionary<string, string[]> Errors { get; }

public ValidationException(Dictionary<string, string[]> errors)
: base("One or more validation failures have occurred")
{
Errors = errors;
}
}

// 401 未授权异常
public class UnauthorizedException : ApplicationException
{
public UnauthorizedException(string message = "Unauthorized")
: base(message)
{
}
}

// 403 禁止访问异常
public class ForbiddenException : ApplicationException
{
public ForbiddenException(string message = "Forbidden")
: base(message)
{
}
}

// 409 冲突异常
public class ConflictException : ApplicationException
{
public ConflictException(string message) : base(message)
{
}
}

使用示例

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;

public ProductsController(IProductService productService)
{
_productService = productService;
}

[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
// 服务层会抛出 NotFoundException
var product = await _productService.GetByIdAsync(id);
return Ok(product);
}

[HttpPost]
public async Task<ActionResult<ProductDto>> Create([FromBody] CreateProductDto dto)
{
// 验证失败会抛出 ValidationException
var product = await _productService.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}

// 服务层
public class ProductService : IProductService
{
private readonly IProductRepository _repository;

public async Task<ProductDto> GetByIdAsync(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
throw new NotFoundException(nameof(Product), id);
}

return product.ToDto();
}

public async Task<ProductDto> CreateAsync(CreateProductDto dto)
{
// 验证
var errors = new Dictionary<string, string[]>();
if (string.IsNullOrWhiteSpace(dto.Name))
{
errors.Add(nameof(dto.Name), new[] { "Name is required" });
}
if (dto.Price <= 0)
{
errors.Add(nameof(dto.Price), new[] { "Price must be greater than 0" });
}

if (errors.Any())
{
throw new ValidationException(errors);
}

var product = dto.ToEntity();
await _repository.AddAsync(product);
return product.ToDto();
}
}

FluentValidation 集成

dotnet add package FluentValidation.AspNetCore
// 验证器
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
public CreateProductDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(200).WithMessage("Name must not exceed 200 characters");

RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0");

RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative");
}
}

// 注册验证器
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductDtoValidator>();

// 验证过滤器
public class ValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(x => x.Value?.Errors.Count > 0)
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray()
);

throw new ValidationException(errors);
}

await next();
}
}

// 注册过滤器
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidationFilter>();
});

Problem Details 标准

public class ProblemDetailsExceptionHandler : IExceptionHandler
{
private readonly ILogger<ProblemDetailsExceptionHandler> _logger;

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

public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "An exception occurred");

var problemDetails = exception switch
{
NotFoundException notFound => new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = "Not Found",
Detail = notFound.Message,
Instance = httpContext.Request.Path
},
ValidationException validation => new ValidationProblemDetails(validation.Errors)
{
Status = StatusCodes.Status400BadRequest,
Title = "Validation Error",
Instance = httpContext.Request.Path
},
_ => new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Server Error",
Detail = "An error occurred while processing your request",
Instance = httpContext.Request.Path
}
};

httpContext.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

return true;
}
}

// .NET 8+ 注册
builder.Services.AddExceptionHandler<ProblemDetailsExceptionHandler>();
builder.Services.AddProblemDetails();

// 使用
app.UseExceptionHandler();

Result 模式

// Result 类
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string Error { get; }

private Result(bool isSuccess, T? value, string error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}

public static Result<T> Success(T value) => new(true, value, string.Empty);
public static Result<T> Failure(string error) => new(false, default, error);
}

// 使用示例
public class ProductService
{
public async Task<Result<ProductDto>> GetByIdAsync(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
{
return Result<ProductDto>.Failure($"Product with id {id} not found");
}

return Result<ProductDto>.Success(product.ToDto());
}
}

[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
var result = await _productService.GetByIdAsync(id);
if (!result.IsSuccess)
{
return NotFound(new { message = result.Error });
}

return Ok(result.Value);
}

最佳实践

异常处理最佳实践
  • ✅ 使用全局异常处理中间件
  • ✅ 定义明确的自定义异常
  • ✅ 在服务层而非控制器抛出异常
  • ✅ 记录所有异常日志
  • ✅ 生产环境不返回堆栈跟踪
  • ✅ 使用 Problem Details 标准
  • ✅ 验证用户输入
  • ✅ 统一错误响应格式
  • ✅ 区分客户端错误和服务器错误
  • ✅ 实现幂等性处理

相关资源