using Dpz.Core.Web.Library.Middleware;
using Microsoft.AspNetCore.Mvc.Filters;
using OpenIddict.Client.AspNetCore;

namespace Dpz.Core.Web.Library.Filter;

public class ExceptionHandleAttribute(ILogger<ExceptionHandleAttribute> logger)
    : ExceptionFilterAttribute
{
    public override async Task OnExceptionAsync(ExceptionContext context)
    {
        await HandleException(context, () => base.OnExceptionAsync(context));
    }

    private Task HandleException(ExceptionContext context, Func<Task> _)
    {
        if (
            context is
            {
                Exception: OperationCanceledException,
                HttpContext.RequestAborted.IsCancellationRequested: true
            }
        )
        {
            context.ExceptionHandled = true;
            return Task.CompletedTask;
        }

        if (context.Exception is SyntheticTimeoutException syntheticTimeoutException)
        {
            logger.LogInformation(syntheticTimeoutException, "timeout");
            context.ExceptionHandled = true;
            context.HttpContext.Response.Clear();
            context.HttpContext.Response.StatusCode = StatusCodes.Status408RequestTimeout;
            return Task.CompletedTask;
        }

        if (context.Exception is CurrentUserRequiredException currentUserRequiredException)
        {
            return HandleCurrentUserRequiredExceptionAsync(context, currentUserRequiredException);
        }

        if (context.Exception is BusinessException businessException)
        {
            logger.LogError(context.Exception, "business error");
            context.Result = new JsonResult(new ResultInfo(businessException.Message));
            context.HttpContext.Response.Clear();
            context.HttpContext.Response.StatusCode = 200;
            return Task.CompletedTask;
        }

        logger.LogError(context.Exception, "Uncaught exception");

        var httpContext = context.HttpContext;
        if (httpContext.Request.ExpectsJsonResponse())
        {
            context.ExceptionHandled = true;
            context.Result = new JsonResult(new ResultInfo(context.Exception.Message));
            context.HttpContext.Response.Clear();
            context.HttpContext.Response.StatusCode = 200;
            return Task.CompletedTask;
        }

        httpContext.Items[HtmlErrorPageMiddleware.ExceptionLoggedItemKey] = true;
        return Task.CompletedTask;
    }

    private async Task HandleCurrentUserRequiredExceptionAsync(
        ExceptionContext context,
        CurrentUserRequiredException exception
    )
    {
        logger.LogInformation(exception, "Current user is required");

        var httpContext = context.HttpContext;
        context.ExceptionHandled = true;
        httpContext.Response.Clear();

        if (httpContext.Request.ExpectsJsonResponse())
        {
            context.Result = new JsonResult(ResultInfo.ToFail(exception.Message))
            {
                StatusCode = StatusCodes.Status401Unauthorized,
            };
            httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return;
        }

        await httpContext.SignOutAsync(Program.AuthorizeCookieName);

        var returnUrl =
            httpContext.Request.PathBase
            + httpContext.Request.Path
            + httpContext.Request.QueryString;
        context.Result = new ChallengeResult(
            OpenIddictClientAspNetCoreDefaults.AuthenticationScheme,
            new AuthenticationProperties { RedirectUri = returnUrl }
        );
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 的全局异常处理过滤器,用于统一处理应用程序中抛出的各种异常。

主要功能

1. 类定义

public class ExceptionHandleAttribute(ILogger<ExceptionHandleAttribute> logger)
    : ExceptionFilterAttribute
  • 继承自 ExceptionFilterAttribute,作为 MVC 的异常过滤器
  • 使用主构造函数(C# 12 特性)注入日志记录器
  • 可以作为特性(Attribute)应用到控制器或方法上

2. 核心异常处理逻辑

HandleException 方法按优先级处理不同类型的异常:

(1) 请求取消异常

if (context.Exception is OperationCanceledException && 
    context.HttpContext.RequestAborted.IsCancellationRequested)
  • 当客户端主动取消请求时触发
  • 标记异常已处理,静默忽略

(2) 合成超时异常

if (context.Exception is SyntheticTimeoutException)
  • 处理自定义的超时异常
  • 返回 408 Request Timeout 状态码
  • 记录信息日志

(3) 用户认证必需异常

if (context.Exception is CurrentUserRequiredException)
  • 需要登录但用户未认证时抛出
  • 调用专门的处理方法(见下文)

(4) 业务异常

if (context.Exception is BusinessException)
  • 处理预期的业务逻辑异常
  • 返回 200 状态码 + JSON 格式的错误信息
  • 记录错误日志

(5) 未捕获的异常

logger.LogError(context.Exception, "Uncaught exception");
  • 处理所有未预期的异常
  • 如果是 JSON 请求(AJAX/API),返回 JSON 错误
  • 如果是普通 HTTP 请求,标记异常供 HtmlErrorPageMiddleware 处理

3. 认证异常专门处理

HandleCurrentUserRequiredExceptionAsync 方法:

private async Task HandleCurrentUserRequiredExceptionAsync(...)

处理流程:

  1. 记录信息日志
  2. 清空响应内容
  3. 如果是 JSON 请求:返回 401 未授权状态
  4. 如果是网页请求
    • 登出当前 Cookie 认证
    • 保存当前请求 URL 作为 returnUrl
    • 重定向到 OpenIddict 登录页面(使用 ChallengeResult

设计特点

分层异常处理:不同异常类型有不同的处理策略
支持 RESTful API:通过 ExpectsJsonResponse() 区分 JSON/HTML 请求
集成认证流程:与 OpenIddict 集成实现自动跳转登录
业务友好:业务异常返回 200 状态码(前端友好)
日志完备:不同级别的日志记录便于问题排查

使用场景

通常在 ASP.NET Core 应用中全局注册:

// Program.cs
builder.Services.AddControllers(options => 
{
    options.Filters.Add<ExceptionHandleAttribute>();
});

或者单独应用到控制器:

[ExceptionHandle]
public class MyController : Controller { }
评论加载中...