前面老周給大伙伴們演示了過濾器的運行流程,大伙只需要知道下麵知識點即可: 1、過濾器分為授權過濾、資源訪問過濾、操作方法(Action)過濾、結果過濾、異常過濾、終結點過濾。上一次咱們沒有說異常過濾和終結點過濾,不過老周後面會說的。對這些過濾器,你有印象就行了。 2、所有過濾器介面都有同步版本和非同步 ...
前面老周給大伙伴們演示了過濾器的運行流程,大伙只需要知道下麵知識點即可:
1、過濾器分為授權過濾、資源訪問過濾、操作方法(Action)過濾、結果過濾、異常過濾、終結點過濾。上一次咱們沒有說異常過濾和終結點過濾,不過老周後面會說的。對這些過濾器,你有印象就行了。
2、所有過濾器介面都有同步版本和非同步版本。為了讓伙伴不要學得太累,咱們暫時只說同步版本的。
3、過濾器的應用可以分為全局和局部。全局先運行,局部後運行。全局在應用程式初始化時配置,局部用特性類來配置。
4、實際應用中,我們不需要實現所有過濾器介面,需要啥就實現啥即可。比如,你想在 Action 調用後修改一些東西,那實現 IActionFilter 介面就好了,其他不用管。
本篇咱們的重點在於“用”,光知道是啥是不行的,得拿來用才是硬道理。
我們先做第一個練習:阻止控制器的參數從查詢字元串獲取數據。
什麼意思呢?咱們知道,MVC 在模型綁定時,會從 N 個 ValueProvider 中提取數據值,包括 QueryString、Forms、RouteData 等。其中,QueryString 就是URL的查詢字元串。比如,咱們寫一個這樣的控制器:
public class GameController : ControllerBase { [HttpGet("game/play")] public string Play(Game g) { if(ModelState.IsValid == false) { return "你玩個寂寞"; } return $"你正在玩{g.Year}年出的《{g.GameName}》游戲"; } } public class Game { /// <summary> /// 游戲序列號 /// </summary> public string? GameSerial { get; set; } /// <summary> /// 游戲名稱 /// </summary> public string? GameName { get; set; } /// <summary> /// 誰發行的 /// </summary> public string? Publisher { get; set; } /// <summary> /// 哪一年發行的 /// </summary> public int Year { get; set; } }
這個通過 /game/play?gameserial=DDSBYCL-5K2FF&gamename=伏地魔三世&publisher=無德無能科技有限公司&year=2017 這樣的URL就能傳遞數據給 g 參數。
這裡我不做 HTML 頁了,直接通過 MapGet 返回 HTML 內容。
app.MapGet("/", async (HttpContext context) => { string html = """ <!DOCTYPE html> <html> <head> <title>試試看</title> <style> label { min-width: 100px; display: inline-block; } </style> </head> <body> <div> <label for="gserial">游戲序列號:</label> <input id="gserial" type="text" /> </div> <div> <label for="gname">游戲名稱:</label> <input id="gname" type="text" /> </div> <div> <label for="pub">發行者:</label> <input type="text" id="pub" /> </div> <div> <label for="year">發行年份:</label> <input id="year" type="text"/> </div> <div> <button onclick="reqTest()">確定</button> </div> <p id="res"></p> <script> function reqTest() { let serial= document.getElementById("gserial").value; let name = document.getElementById("gname").value; let pub = document.getElementById("pub").value; let year = parseInt(document.getElementById("year").value, 10); let result = document.getElementById("res"); const url = `/game/play?gameSerial=${serial}&gamename=${name}&publisher=${pub}&year=${year}`; fetch(url, { method: "GET" }) .then(response => { response.text().then(txt => { result.innerHTML = txt; }); }); } </script> </body> </html> """; var response = context.Response; response.Headers.ContentType = "text/html; charset=UTF-8"; await response.WriteAsync(html); });
設置響應的 Content-Type 頭時一定要指定字元集是 UTF-8 編碼,這樣可以免去 99.999% 的亂碼問題。向伺服器發送請求是通過 fetch 函數實現的。
比如,咱們在頁面上填寫:
然後點一下“確定”按鈕,提交成功後服務將響應:
你正在玩2022年出的《法外狂徒大冒險》游戲
模型綁定的數據是從查詢字元串提取出來的。現在,咱們寫一個過濾器,阻止 QueryStringValueProvider 提供查詢字元串數據。而 QueryStringValueProvider 實例是由 QueryStringValueProviderFactory 工廠類負責創建的。因此,需要寫一個過濾器,在模型綁定之前刪除 QueryStringValueProviderFactory 對象,這樣模型綁定時就不會讀取 URL 中的查詢字元串了。
於是,重點就落在選用哪種過濾器。關鍵點是:必須在模型綁定前做這項工作。所以,Action過濾器、結果過濾器就別指望了,而且肯定不是授權過濾器,那就剩下資源過濾器了。
咱們寫一個自定義的資源過濾器—— RemoveQueryStringProviderFilter,實現的介面當然是 IResourceFilter 了。
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; public class RemoveQueryStringProviderFilter : IResourceFilter { public void OnResourceExecuted(ResourceExecutedContext context) { // 空空如也 } public void OnResourceExecuting(ResourceExecutingContext context) { var qsValueProviders = context.ValueProviderFactories.OfType<QueryStringValueProviderFactory>(); if (qsValueProviders != null && qsValueProviders.Any()) { context.ValueProviderFactories.RemoveType<QueryStringValueProviderFactory>(); } } }
我們要做的事情是在模型綁定之前才有效,所以 OnResourceExecuted 方法不用管,留白即可。在 OnResourceExecuting 方法中,首先用 ValueProviderFactories.OfType<T> 方法找出現有的 QueryStringValueProviderFactory 對象,若找到,用 RemoveType 方法刪除。
把這個過濾器應用於全局。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.Filters.Add<RemoveQueryStringProviderFilter>(); }); var app = builder.Build();
現在,咱們再運行應用程式,輸入游戲信息。
點擊“確定”按鈕後,發現服務未響應正確的內容。
你正在玩0年出的《》游戲
由於無法從查詢字元串中提取到數據,所以返回預設屬性值。為什麼不是返回“你玩個寂寞”呢?因為模型綁定並沒有出錯,也未出現驗證失敗的值,只是未提供值而已,即 ModelState.IsValid 的值依然是 true 的。
下麵咱們看看異常過濾器怎麼用。
異常過濾器最好定義為局部的,而非全局。使用局部過濾器的好處是針對性好,全局的話你用一個萬能異常處理好像過於簡單。當然了,如果你的項目可以這樣做,就隨便用。
這裡我定義一個局部的異常過濾器,通過特性方式應用到操作方法上。
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class CustExceptionFilterAttribute : Attribute, IExceptionFilter { public void OnException(ExceptionContext context) { // 看看有沒有異常 if (context.Exception is null || context.ExceptionHandled) return; // 有異常哦,修改一下返回結果 ContentResult result = new(); result.Content = "出錯啦,伙計。錯誤消息:" + context.Exception.Message; // 設置返回結果 context.Result = result; // 標記異常已處理 context.ExceptionHandled = true; } }
首先,context 參數是一種上下文對象,它在多上異常過濾器間共用實例(你可能實現了很多個異常過濾器)。context.Exception 屬性引用的是異常類對象;註意一個有趣的屬性 ExceptionHandled,它是一個 bool 類型的值,表示“我這個異常過濾器是否已處理過了”。這個有啥用呢?由於上下文是共用的,當你的某個異常過濾器設置了 ExceptionHandled 為 true,那麼,其他異常過濾器也可以讀這個屬性。這樣就可以實現:啊,原來有人處理過這個異常了,那我就不處理了。即 A 過濾器處理異常後設置為已處理,B 過濾器可以檢查這個屬性的值,如果沒必要處理就跳過。
下麵寫一個控制器,併在方法成員上應用前面定義的異常過濾器。
public class AbcController : ControllerBase { [HttpGet("calc/add"), CustExceptionFilter] public int Add(int x, int y) { int r = x + y; if(r >= 1000) { throw new Exception("計算結果必須在1000以內"); } return r; } }
此處我設定拋出異常的條件是:x、y 相加結果大於或等於 1000。
咱們以 GET 方式調用,URL 為 /calc/add?x=599&y=699,這樣會發生異常。伺服器的響應為:
最後一個例子是結果過濾器。咱們要實現在響應消息中添加自定義 Cookie。
public class SetCookieResultFilter : IResultFilter { public void OnResultExecuted(ResultExecutedContext context) { // 不能在這裡寫 Cookie } public void OnResultExecuting(ResultExecutingContext context) { HttpResponse response = context.HttpContext.Response; // 設置Cookie response.Cookies.Append("X-VER", "3.0"); } }
這裡要註意,咱們要寫 Cookie 必須在 OnResultExecuting 方法中處理,不能在 OnResultExecuted 方法中寫。因為當 OnResultExecuted 方法執行時,響應消息頭已經被鎖定了,Cookie 是通過 set-cookie 標頭實現的,此時無法修改 HTTP 頭了,寫 Cookie 會拋異常。所以只能在 OnResultExecuting 方法中寫。
定義一個控制器。
public class DemoController : ControllerBase { [HttpGet("abc/test")] public string Work() => "山重水複疑無路"; }
將自定義的結果過濾器添加為全局。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.Filters.Add<SetCookieResultFilter>(); }); var app = builder.Build();
好,試試看。