ASP.NET Core Filters

ASP.NET MVC 中的過濾器(Filter)是 AOP(面向切面編程) 思想的一種實現,供我們在執行管道的特定階段執行代碼,通過使用過濾器可以實現 短路請求、緩存請求結果、日誌統一記錄、參數合法性驗證、異常統一處理、返回值格式化 等等,同時使業務代碼更加簡潔單純,避免很多重複代碼。

過濾器執行流程

MVC 在選擇要執行的 Action 方法後,會執行過濾器管道,流程如下圖:

MVC 調用管道

過濾器作用域

過濾器作用域設置是非常靈活的,可以選擇爲:

  1. 全局有效(整個 MVC 應用程序下的每一個 Action);

  2. 僅對某些 Controller 有效 (控制器內所有的 Action );

  3. 僅對某些 Action 有效;

過濾器類型

過濾器分爲 Authorization Filter、Resource Filter、Action Filter、Exception Filter、Result Filter,每種過濾器都有其應用場景,不同類型的過濾器會在執行管道的不同階段運行,我們需要根據實際情況來選擇使用。

Authorization Filter

授權過濾器在過濾器管道中第一個被執行,通常用於驗證請求的合法性(通過實現接口 IAuthorizationFilter or IAsyncAuthorizationFilter

Resource Filter

資源過濾器在過濾器管道中第二個被執行,通常用於請求結果的緩存和短路過濾器管道(通過實現接口 IResourceFilter or IAsyncResourceFilter

Action Filter

Acioin 過濾器可設置在調用 Acioin 方法之前和之後執行代碼,如:請求參數的驗證(通過實現接口 IActionFilter or IAsyncActionFilter

Exception Filter

程序異常信息處理(通過實現接口 IExceptionFilter or IAsyncExceptionFilter

Result Filter

Action 執行完成後執行,對執行結果格式化處理(通過實現接口 IResultFilter or IAsyncResultFilter

public class AddHeaderResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("AddHeaderResultFilter:OnResultExecuted");
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add("ResultFilter", new string[] { "AddHeader" });

        context.Result = new ObjectResult(new ApiResult<string>()
        {
            Code = Enum.ResultCode.Success,
            Data = "我是 AddHeaderResultFilter 修改後的值"
        });
    }
}

Filter Attributes

將過濾器接口的實現以特性(Attributes) 的方式使用是非常方便的,過濾器特性可應用於 Controller 和 Action 。框架包含了內置的基於特性的過濾器,我們可以直接繼承或另外定製。

繼承內置的 ExceptionFilterAttribute:

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        context.Result = new ObjectResult(new ApiResult()
        {
            Code = Enum.ResultCode.Exception,
            ErrorMessage = $"CustomExceptionFilterAttribute: {context.Exception.Message}"
        });
    }
}
[CustomExceptionFilter]
public ApiResult ExceptionAttributeTest()
{
    throw new Exception("Boom");
}

定製 ResourceFilterAttribute:

public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        context.Result = new ObjectResult(new ApiResult<string>()
        {
            Code = Enum.ResultCode.Failed,
            Data = "我是 ShortCircuitingResourceFilterAttribute 的返回值"
        });
    }
}

過濾器實戰

問題:在開發中都存在這樣的場景,方法參數需要添加一些 if 的判斷,不合法直接返回錯誤信息,方法體加個 try\catch,捕獲到異常後記錄日誌,而這些基本是每一個接口方法必須的,實際情況下我們可能是 ctrl+c & ctrl+v ,部分代碼微調,搞定,並不會花什麼時間,但我們可以看看整個文件的代碼,大部分都是一樣的架子,頭部驗證,尾部捕獲,中間調用主邏輯層的方法,滿滿的重複代碼,而且如果後期要調整基礎架子,每個方法都得來一遍,那必須得蛋疼死。

解決方案: Action Filter + Exception Filter

  1. 創建一個 Filter,同時實現 IAsyncActionFilter 和 IAsyncExceptionFilter:
    public class XXXActionFilter : IAsyncActionFilter, IAsyncExceptionFilter
    {
        private IDictionary<string, object> _actionArguments;
    
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // 如果參數驗證不通過,返回參數錯誤的信息
            if (!context.ModelState.IsValid)
            {
                var errorMessages = new List<string>();
                foreach (var key in context.ModelState.Keys)
                {
                    var state = context.ModelState[key];
                    var errorModel = state?.Errors?.First();
                    if (errorModel != null)
                        errorMessages.Add($"{key}:{errorModel.ErrorMessage}");
                }
                var result = new ApiResult()
                {
                    Code = ResultCode.ArgumentError,
                    Message = errorMessages.Join(",")
                };
                context.Result = new ObjectResult(result);
                return;
            }
    
            // 保存下來,ExceptionContext 中取不到,如果出異常了需要記錄請求參數
            _actionArguments = context.ActionArguments;
    
            await next();
        }
    
        public async Task OnExceptionAsync(ExceptionContext context)
        {
            
            // 記錄異常日誌 
            // some code..........
            
            var result = new ApiResult()
            {
                Code = ResultCode.Exception,
                Message = $"{context.ActionDescriptor.RouteValues["action"]} Exception")
            };
            context.Result = new ObjectResult(result);
            await Task.CompletedTask;
        }
    }
    
    
  2. Startup.cs 的 ConfigureServices 中添加 XXXActionFilter 爲全局有效:
    services.AddMvc(options =>
    {
        options.Filters.Add<XXXActionFilter>();
    });
    
  3. Controller 中的添加測試方法 :
    public ApiResult ActionTest(XXXRequest request)
    {
        if (request.Id == "1")
        {
            throw new Exception("xxxx");
        }
        return new ApiResult<string>()
        {
            Code = Enum.ResultCode.Success,
            Data = "ActionTest"
        };
    }
    
    public class XXXRequest
    {
        [Required]
        public string Id { get; set; }
    }
    

調用 ActionTest 進行測試,如果 id 沒傳,會進去 OnActionExecutionAsync 的 context.ModelState.IsValid 爲 fasle 的情況:

invalid

如果 id 爲1,會進入 OnExceptionAsync:

exception

其他情況下正常返回:

success

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章