Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
JavaScript編程中,錯(cuò)誤處理是不可或缺的一部分。良好的錯(cuò)誤處理可以讓我們的應(yīng)用更加健壯和用戶友好。try...catch語(yǔ)句是JavaScript中處理運(yùn)行時(shí)錯(cuò)誤的一種基本方式。本文將通過(guò)幾個(gè)實(shí)例來(lái)展示如何在HTML5中使用try...catch來(lái)捕獲和處理錯(cuò)誤。
try...catch語(yǔ)句包含兩個(gè)部分:try塊和catch塊。
如果try塊中的代碼運(yùn)行正常,則跳過(guò)catch塊。如果try塊中的代碼拋出錯(cuò)誤,則立即停止執(zhí)行try塊中的剩余代碼,并跳轉(zhuǎn)到catch塊。
try {
// 嘗試執(zhí)行的代碼
} catch (error) {
// 發(fā)生錯(cuò)誤時(shí)執(zhí)行的代碼
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>try...catch 示例1</title>
</head>
<body>
<script>
try {
eval('alert("Hello world)'); // 缺少引號(hào)導(dǎo)致的語(yǔ)法錯(cuò)誤
} catch (error) {
console.error('捕獲到錯(cuò)誤:', error.message);
}
</script>
</body>
</html>
在這個(gè)例子中,我們嘗試使用eval函數(shù)執(zhí)行一段代碼,但由于字符串沒(méi)有閉合,導(dǎo)致了語(yǔ)法錯(cuò)誤。try...catch捕獲到這個(gè)錯(cuò)誤,并在控制臺(tái)輸出了錯(cuò)誤信息。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>try...catch 示例2</title>
</head>
<body>
<script>
try {
var json = '{name:"John Doe"'; // JSON格式不正確
var user = JSON.parse(json);
console.log(user.name);
} catch (error) {
console.error('JSON解析錯(cuò)誤:', error.message);
}
</script>
</body>
</html>
在這個(gè)例子中,我們嘗試解析一個(gè)不正確的JSON字符串。JSON.parse在嘗試解析時(shí)會(huì)拋出錯(cuò)誤,try...catch捕獲到這個(gè)錯(cuò)誤,并在控制臺(tái)輸出了錯(cuò)誤信息。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>try...catch 示例3</title>
</head>
<body>
<script>
try {
var elem = document.getElementById('myElement');
elem.innerHtml = 'Hello World'; // 正確的屬性是innerHTML
} catch (error) {
console.error('DOM操作錯(cuò)誤:', error.message);
}
</script>
</body>
</html>
在這個(gè)例子中,我們嘗試設(shè)置一個(gè)不存在的DOM元素的innerHtml屬性,這會(huì)導(dǎo)致一個(gè)TypeError,因?yàn)閑lem是null。try...catch捕獲到這個(gè)錯(cuò)誤,并在控制臺(tái)輸出了錯(cuò)誤信息。
finally塊是try...catch結(jié)構(gòu)的一個(gè)可選部分,無(wú)論是否發(fā)生錯(cuò)誤,finally塊中的代碼總是會(huì)被執(zhí)行。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>try...catch 示例4</title>
</head>
<body>
<script>
try {
// 一些可能會(huì)拋出錯(cuò)誤的代碼
} catch (error) {
// 處理錯(cuò)誤
} finally {
// 清理或完成工作的代碼
console.log('無(wú)論是否發(fā)生錯(cuò)誤,這段代碼都會(huì)執(zhí)行');
}
</script>
</body>
</html>
在這個(gè)例子中,無(wú)論try塊中的代碼是否拋出錯(cuò)誤,finally塊中的console.log都會(huì)被執(zhí)行。
try...catch是處理JavaScript中錯(cuò)誤的有效方式,它可以幫助我們捕獲運(yùn)行時(shí)錯(cuò)誤,并根據(jù)需要進(jìn)行處理。通過(guò)合理使用try...catch,我們的應(yīng)用程序可以更加健壯和可靠。記住,錯(cuò)誤處理不僅僅是捕獲錯(cuò)誤,更重要的是如何根據(jù)不同的錯(cuò)誤類型給用戶提供有用的反饋和恢復(fù)程序的運(yùn)行。
章涵蓋
為簡(jiǎn)單起見(jiàn),到目前為止,我們假設(shè)來(lái)自客戶端的數(shù)據(jù)始終正確且足以滿足 Web API 的終結(jié)點(diǎn)。不幸的是,情況并非總是如此:無(wú)論我們喜歡與否,我們經(jīng)常必須處理錯(cuò)誤的HTTP請(qǐng)求,這可能是由多種因素(包括惡意攻擊)引起的,但總是因?yàn)槲覀兊膽?yīng)用程序面臨意外或未經(jīng)處理的行為而發(fā)生。
在本章中,我們將討論在客戶端-服務(wù)器交互期間處理意外情況的一系列技術(shù)。這些技術(shù)依賴于兩個(gè)主要概念:
在接下來(lái)的部分中,我們將了解如何在代碼中將它們付諸實(shí)踐。
我們從第1章中知道,Web API的主要目的是使不同的各方能夠通過(guò)交換信息進(jìn)行交互。在后面的章節(jié)中,我們看到了如何實(shí)現(xiàn)幾個(gè)可用于創(chuàng)建、讀取、更新和刪除數(shù)據(jù)的 HTTP 端點(diǎn)。這些終結(jié)點(diǎn)中的大多數(shù)(如果不是全部)都需要來(lái)自調(diào)用客戶端的某種輸入。例如,考慮 GET /BoardGames 端點(diǎn)所需的參數(shù),我們?cè)诘?5 章中對(duì)此進(jìn)行了極大的改進(jìn):
所有這些參數(shù)都是可選的。我們選擇允許它們不存在(換句話說(shuō),在沒(méi)有它們的情況下接受傳入請(qǐng)求),因?yàn)槲覀兛梢暂p松提供合適的默認(rèn)值,以防調(diào)用方未顯式提供它們。因此,以下所有 HTTP 請(qǐng)求都將以相同的方式處理,因此將提供相同的結(jié)果(直到默認(rèn)值更改):
同時(shí),我們要求其中一些參數(shù)的值與給定的 .NET 類型兼容,而不是原始字符串。pageIndex 和 pageSize 就是這種情況,它們的值應(yīng)為整數(shù)類型。如果我們嘗試傳遞其中一個(gè)不兼容的值,例如 HTTP 請(qǐng)求 https://localhost:40443/BoardGames?pageIndex=test,我們的應(yīng)用程序?qū)㈨憫?yīng) HTTP 400 - 錯(cuò)誤請(qǐng)求錯(cuò)誤,甚至不開(kāi)始執(zhí)行 BoardGamesController 的 Get 操作方法:
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title":"One or more validation errors occurred.",
"status":400,
"traceId":"00-a074ebace7131af6561251496331fc65-ef1c633577161417-00",
"errors":{
"pageIndex":["The value 'string' is not valid."]
}
}
我們可以很容易地看到,通過(guò)允許和/或拒絕此類請(qǐng)求,我們已經(jīng)通過(guò)主動(dòng)檢查兩個(gè)重要的驗(yàn)收標(biāo)準(zhǔn)對(duì)這些參數(shù)執(zhí)行了某種數(shù)據(jù)驗(yàn)證活動(dòng):
我們顯然是在談?wù)撾[式活動(dòng),因?yàn)榭諜z查和回退到默認(rèn)值的任務(wù)是由底層框架執(zhí)行的,而無(wú)需我們編寫任何內(nèi)容。具體來(lái)說(shuō),我們正在利用 ASP.NET Core的模型綁定系統(tǒng),這是自動(dòng)處理所有這些的機(jī)制。
來(lái)自 HTTP 請(qǐng)求的所有輸入數(shù)據(jù)(請(qǐng)求標(biāo)頭、路由數(shù)據(jù)、查詢字符串、表單字段等)都通過(guò)原始字符串傳輸并接收。ASP.NET Core 框架檢索這些值,并自動(dòng)將它們從字符串轉(zhuǎn)換為 .NET 類型,從而使開(kāi)發(fā)人員免于繁瑣、容易出錯(cuò)的手動(dòng)活動(dòng)。具體而言,模型綁定系統(tǒng)從 HTTP 請(qǐng)求查詢字符串和/或正文中檢索輸入數(shù)據(jù),并將其轉(zhuǎn)換為強(qiáng)類型方法參數(shù)。此過(guò)程在每個(gè) HTTP 請(qǐng)求時(shí)自動(dòng)執(zhí)行,但可以根據(jù)開(kāi)發(fā)人員的要求配置一組基于屬性的約定。
讓我們看看模型綁定在后臺(tái)的作用。考慮HTTP GET請(qǐng)求 https://localhost:40443/BoardGames?pageIndex=2&pageSize=50,它被路由到我們的BoardGamesController的Get操作方法:
public async Task<RestDTO<BoardGame[]>> Get(
int pageIndex = 0,
int pageSize = 10,
string? sortColumn = "Name",
string? sortOrder = "ASC",
string? filterQuery = null)
模型綁定系統(tǒng)執(zhí)行以下任務(wù):
簡(jiǎn)而言之,模型綁定系統(tǒng)的主要用途是將給定的(原始字符串)源轉(zhuǎn)換為一個(gè)或多個(gè)預(yù)期的(.NET 類型)目標(biāo)。在我們的示例中,URL 發(fā)出的原始 GET 參數(shù)是模型綁定的源,操作方法的類型化參數(shù)是目標(biāo)。目標(biāo)可以是簡(jiǎn)單類型(整數(shù)、布爾值等)或復(fù)雜類型(如數(shù)據(jù)傳輸對(duì)象 [DTO]),我們將在后面看到。
除了執(zhí)行標(biāo)準(zhǔn)類型轉(zhuǎn)換之外,還可以將模型綁定配置為使用 System.ComponentModel.DataAnnotation 命名空間中包含的一組內(nèi)置數(shù)據(jù)批注屬性來(lái)執(zhí)行多個(gè)數(shù)據(jù)驗(yàn)證任務(wù)。以下是這些屬性中最值得注意的列表:
學(xué)習(xí)如何使用這些驗(yàn)證屬性的最佳方法是在我們的MyBGList Web API中實(shí)現(xiàn)它們。假設(shè)我們希望(或被要求)將 GET /BoardGames 端點(diǎn)的頁(yè)面大小限制為最大值 100。以下是我們?nèi)绾瓮ㄟ^(guò)使用 [Range] 屬性來(lái)做到這一點(diǎn):
public async Task<RestDTO<BoardGame[]>> Get(
int pageIndex = 0,
[Range(1, 100)] int pageSize = 10, ?
string? sortColumn = "Name",
string? sortOrder = "ASC",
string? filterQuery = null)
? 范圍驗(yàn)證器(1 至 100)
注意此更改請(qǐng)求是可信的。無(wú)限制地接受任何頁(yè)面大小意味著允許可能昂貴的數(shù)據(jù)檢索請(qǐng)求,這可能會(huì)導(dǎo)致 HTTP 響應(yīng)延遲、速度減慢和性能下降,從而使我們的 Web 應(yīng)用程序面臨拒絕服務(wù) (DoS) 攻擊。
此更改將導(dǎo)致 URL https://localhost:40443/BoardGames?pageSize=200 返回 HTTP 400 - 錯(cuò)誤請(qǐng)求狀態(tài)錯(cuò)誤,而不是前 200 個(gè)棋盤游戲。正如我們很容易理解的那樣,每當(dāng)我們想在輸入數(shù)據(jù)周圍放置一些邊界而不手動(dòng)實(shí)施相應(yīng)的檢查時(shí),數(shù)據(jù)注釋屬性都很有用。如果我們不想使用 [Range] 屬性,我們可以使用以下代碼獲得相同的結(jié)果:
if (pageSize < 0 || pageSize > 100) {
// .. do something
}
該“某些內(nèi)容”可以通過(guò)各種方式實(shí)現(xiàn),例如引發(fā)異常,返回HTTP錯(cuò)誤狀態(tài)或執(zhí)行任何其他合適的錯(cuò)誤處理操作。但是,手動(dòng)方法可能難以維護(hù),并且通常容易出現(xiàn)人為錯(cuò)誤。出于這個(gè)原因,使用 ASP.NET Core 的最佳實(shí)踐是采用框架提供的集中式接口,只要我們可以使用它來(lái)實(shí)現(xiàn)我們需要做的事情。
定義這種方法被稱為面向方面的編程(AOP),這是一種范式,旨在通過(guò)向現(xiàn)有代碼添加行為而不修改代碼本身來(lái)提高源代碼的模塊化。ASP.NET 提供的數(shù)據(jù)注釋屬性就是一個(gè)很好的例子,因?yàn)樗鼈冊(cè)试S開(kāi)發(fā)人員添加功能而不會(huì)使代碼混亂。
我們用來(lái)限制頁(yè)面大小的 [Range(1, 100)] 驗(yàn)證器很容易實(shí)現(xiàn)。讓我們嘗試一個(gè)更困難的更改請(qǐng)求。假設(shè)我們希望(或被要求)驗(yàn)證 sortOrder 參數(shù),該參數(shù)當(dāng)前接受任何字符串,以僅接受可被視為對(duì)其特定目的有效的值,即“ASC”或“DESC”。同樣,此更改請(qǐng)求非常合理。接受參數(shù)(如 sortOrder)的任意字符串值(以編程方式用于使用動(dòng)態(tài) LINQ 編寫 LINQ 表達(dá)式)可能會(huì)使我們的 Web 應(yīng)用程序面臨危險(xiǎn)的漏洞,例如 SQL 注入或 LINQ 注入。出于這個(gè)原因,為我們的應(yīng)用程序提供這些“動(dòng)態(tài)”字符串的驗(yàn)證器是我們應(yīng)該注意的安全要求。
提示有關(guān)此主題的其他信息,請(qǐng)查看 http://mng.bz/Q8w4 中的 StackOverflow 線程。
同樣,我們可以通過(guò)采用編程方法輕松實(shí)現(xiàn)此更改請(qǐng)求,在操作方法本身中進(jìn)行以下“手動(dòng)檢查”:
if (sortOrder != "ASC" && sortOrder != "DESC") {
// .. do something
}
但是我們至少有兩種其他方法可以使用 ASP.NET Core 提供的內(nèi)置驗(yàn)證器接口實(shí)現(xiàn)相同的結(jié)果:使用 [RegularExpression] 屬性或?qū)崿F(xiàn)自定義驗(yàn)證屬性。在接下來(lái)的部分中,我們將使用這兩種技術(shù)。
使用正則表達(dá)式屬性
類是最有用和最可自定義的數(shù)據(jù)批注屬性之一,因?yàn)樗褂昧苏齽t表達(dá)式的強(qiáng)大功能和靈活性。[RegularExpression] 屬性依賴于 .NET 正則表達(dá)式引擎,該引擎由 System.Text.RegularExpressions 命名空間及其 Regex 類表示。此引擎接受使用 Perl 5 兼容語(yǔ)法編寫的正則表達(dá)式模式,并根據(jù)所使用的方法將它們用于輸入字符串以確定匹配項(xiàng)、檢索匹配項(xiàng)或替換匹配文本。具體來(lái)說(shuō),該屬性在內(nèi)部調(diào)用 IsMatch() 方法來(lái)確定模式是否在輸入字符串中找到匹配項(xiàng),這正是我們場(chǎng)景中所需要的。
正則表達(dá)式
正則表達(dá)式(也稱為 RegEx 和 RegExp)是用于匹配字符串中的字符組合的標(biāo)準(zhǔn)化模式。該技術(shù)起源于 1951 年,但直到 1980 年代后期才開(kāi)始流行,這要?dú)w功于 Perl 語(yǔ)言(自 1986 年以來(lái)一直以正則表達(dá)式庫(kù)為特色)的全球采用。此外,在1990年代,Perl兼容正則表達(dá)式(PCRE)庫(kù)被許多現(xiàn)代工具(如PHP和Apache HTTP Server)采用,成為事實(shí)上的標(biāo)準(zhǔn)。
在本書中,我們很少使用正則表達(dá)式,只是在基本程度上使用。要了解有關(guān)該主題的更多信息,請(qǐng)查看以下網(wǎng)站,該網(wǎng)站提供了一些有見(jiàn)地的教程、示例和快速入門指南:https://www.regular-expressions.info。
下面是一個(gè)合適的正則表達(dá)式模式,我們可以用來(lái)檢查是否存在 ASC 或 DESC 字符串:
ASC|DESC
此模式可以通過(guò)以下方式在 BoardGamesController 的 Get 操作方法中的 [RegularExpression] 屬性中使用:
public async Task<RestDTO<BoardGame[]>> Get(
int pageIndex = 0,
[Range(1, 100)] int pageSize = 10,
string? sortColumn = "Name",
[RegularExpression("ASC|DESC")] string? sortOrder = "ASC",
string? filterQuery = null)
之后,包含不同于“ASC”和“DESC”的sortOrder參數(shù)值的所有傳入請(qǐng)求都將被視為無(wú)效,從而導(dǎo)致HTTP 400 - 錯(cuò)誤請(qǐng)求響應(yīng)。
使用自定義驗(yàn)證屬性
如果我們不想使用 [RegularExpression] 屬性來(lái)滿足我們的更改請(qǐng)求,我們可以使用自定義驗(yàn)證屬性實(shí)現(xiàn)相同的結(jié)果。所有現(xiàn)有的驗(yàn)證屬性都擴(kuò)展了 ValidationAttribute 基類,該基類提供了一個(gè)方便(且可重寫)的 IsValid() 方法,該方法執(zhí)行實(shí)際的驗(yàn)證任務(wù)并返回包含結(jié)果的 ValidationResult 對(duì)象。要實(shí)現(xiàn)我們自己的驗(yàn)證屬性,我們需要執(zhí)行以下步驟:
添加 SortOrderValidator 類文件
在Visual Studio的解決方案資源管理器中,在MyBGList項(xiàng)目的根目錄中創(chuàng)建一個(gè)新的/Attributes/文件夾。然后右鍵單擊該文件夾,并添加新的 SortOrderValidatorAttribute.cs 類文件,以在新的 MyBGList.Attributes 命名空間中生成一個(gè)空樣板。現(xiàn)在,我們已準(zhǔn)備好實(shí)現(xiàn)自定義驗(yàn)證器。
實(shí)現(xiàn) SortOrderValidator
下面的清單提供了一個(gè)最小實(shí)現(xiàn),該實(shí)現(xiàn)根據(jù)“ASC”和“DESC”值檢查輸入字符串,僅當(dāng)其中一個(gè)值完全匹配時(shí),才返回成功的結(jié)果。
清單 6.1 排序順序驗(yàn)證器屬性
using System.ComponentModel.DataAnnotations;
namespace MyBGList.Attributes
{
public class SortOrderValidatorAttribute : ValidationAttribute
{
public string[] AllowedValues { get; set; } =
new[] { "ASC", "DESC" };
public SortOrderValidatorAttribute()
: base("Value must be one of the following: {0}.") { }
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
var strValue = value as string;
if (!string.IsNullOrEmpty(strValue)
&& AllowedValues.Contains(strValue))
return ValidationResult.Success;
return new ValidationResult(
FormatErrorMessage(string.Join(",", AllowedValues))
);
}
}
}
代碼易于閱讀。根據(jù)允許的字符串值數(shù)組(AllowedValues 字符串?dāng)?shù)組)檢查輸入值,以確定它是否有效。請(qǐng)注意,如果驗(yàn)證失敗,生成的 ValidationResult 對(duì)象將使用方便的錯(cuò)誤消息進(jìn)行實(shí)例化,該消息將為調(diào)用方提供有關(guān)失敗檢查的一些有用的上下文信息。此消息的默認(rèn)文本在構(gòu)造函數(shù)中定義,但我們可以使用 ValidationAttribute 基類提供的公共 ErrorMessage 屬性按以下方式更改它:
[SortOrderValidator(ErrorMessage = "Custom error message")]
此外,我們將 AllowedValues 字符串?dāng)?shù)組屬性設(shè)置為 public,這使我們有機(jī)會(huì)通過(guò)以下方式自定義這些值:
[SortOrderValidator(AllowedValues = new[] { "ASC", "DESC", "OtherString" })]
提示自定義允許的排序值在某些邊緣情況下可能很有用,例如將 SQL Server 替換為支持不同排序語(yǔ)法的數(shù)據(jù)庫(kù)管理系統(tǒng) (DBMS)。這就是我們?yōu)樵搶傩远x set 訪問(wèn)器的原因。
現(xiàn)在我們可以回到 BoardGamesController 的 Get 方法,并將我們之前添加的 [RegularExpression] 屬性替換為新的 [SortOrderValidator] 自定義屬性:
using MyBGList.Attributes;
// ...
public async Task<RestDTO<BoardGame[]>> Get(
int pageIndex = 0,
[Range(1, 100)] int pageSize = 10,
string? sortColumn = "Name",
[SortOrderValidator] string? sortOrder = "ASC",
string? filterQuery = null)
實(shí)現(xiàn) SortColumnValidator
在繼續(xù)之前,讓我們實(shí)現(xiàn)另一個(gè)自定義驗(yàn)證器來(lái)修復(fù)正在進(jìn)行的 BoardGamesControlle 的 Get 操作方法中的其他安全問(wèn)題:sortColumn 參數(shù)。同樣,我們必須處理用于動(dòng)態(tài)構(gòu)建 LINQ 表達(dá)式樹(shù)的任意用戶提供的字符串參數(shù),這可能會(huì)使我們的 Web 應(yīng)用程序受到一些 LINQ 注入攻擊。為了防止這些類型的威脅,我們至少可以做的是相應(yīng)地驗(yàn)證該字符串。
但是,這一次,“允許”值由 [BoardGame] 數(shù)據(jù)庫(kù)表的屬性確定,該表在我們的代碼庫(kù)中由 BoardGame 實(shí)體表示。我們可以采取以下兩種方法之一:
第一種方法很容易通過(guò)使用 [RegularExpression] 屬性或類似于我們創(chuàng)建的 SortOrderValidator 的自定義屬性來(lái)實(shí)現(xiàn)。但是,從長(zhǎng)遠(yuǎn)來(lái)看,此解決方案可能很難維護(hù),特別是如果我們計(jì)劃向 BoardGame 實(shí)體添加更多屬性。此外,除非我們每次都將整套“有效”固定字符串作為參數(shù)傳遞,否則它不夠靈活,無(wú)法與域、力學(xué)等實(shí)體一起使用。
動(dòng)態(tài)方法可能是更好的選擇,特別是考慮到我們可以讓它接受 EntityType 屬性,我們可以使用它來(lái)傳遞要檢查的實(shí)體類型。然后,使用 LINQ 循環(huán)訪問(wèn)所有 EntityType 的屬性以檢查其中一個(gè)屬性是否與輸入字符串匹配,這將很容易。下面的清單顯示了我們?nèi)绾卧谝粋€(gè)新的 SortColumnValidatorAttribute.cs 文件中實(shí)現(xiàn)這種方法。
清單 6.2 排序列驗(yàn)證器屬性
using System.ComponentModel.DataAnnotations;
namespace MyBGList.Attributes
{
public class SortColumnValidatorAttribute : ValidationAttribute
{
public Type EntityType { get; set; }
public SortColumnValidatorAttribute(Type entityType)
: base("Value must match an existing column.")
{
EntityType = entityType;
}
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
if (EntityType != null)
{
var strValue = value as string;
if (!string.IsNullOrEmpty(strValue)
&& EntityType.GetProperties()
.Any(p => p.Name == strValue))
return ValidationResult.Success;
}
return new ValidationResult(ErrorMessage);
}
}
}
如我們所見(jiàn),IsValid() 方法源代碼的核心部分依賴于 GetProperties() 方法,該方法返回與類型屬性對(duì)應(yīng)的 PropertyInfo 對(duì)象數(shù)組。
警告正如我們已經(jīng)實(shí)現(xiàn)的那樣,IsValid() 方法將考慮任何對(duì)排序目的有效的屬性,只要它存在:盡管這種方法在我們的特定場(chǎng)景中可能有效,但在處理具有私有屬性的實(shí)體、包含個(gè)人或敏感數(shù)據(jù)的公共屬性等時(shí),它并不是最安全的選擇。為了更好地了解此潛在問(wèn)題,請(qǐng)考慮使用具有包含密碼哈希的密碼屬性的用戶實(shí)體。我們不希望允許客戶端使用該屬性對(duì)用戶列表進(jìn)行排序,對(duì)嗎?這些問(wèn)題可以通過(guò)調(diào)整前面的實(shí)現(xiàn)以顯式排除某些屬性來(lái)解決,或者(更好地)通過(guò)強(qiáng)制實(shí)施在與客戶端交互時(shí)始終使用 DTO 而不是實(shí)體類的良好做法來(lái)解決,除非我們 100% 確定實(shí)體的數(shù)據(jù)不會(huì)構(gòu)成任何威脅。
我們?cè)诖颂幨褂玫囊跃幊谭绞阶x取/檢查代碼元數(shù)據(jù)的技術(shù)稱為反射。大多數(shù)編程框架通過(guò)一組專用庫(kù)或模塊支持它。在 .NET 中,此方法可通過(guò) System.Reflection 命名空間提供的類和方法使用。
提示有關(guān)反射技術(shù)的其他信息,請(qǐng)查看以下指南:http://mng.bz/X57E。
現(xiàn)在我們有了新的自定義驗(yàn)證屬性,我們可以通過(guò)以下方式在 BoardGamesController 的 Get 方法中使用它:
public async Task<RestDTO<BoardGame[]>> Get(
int pageIndex = 0,
[Range(1, 100)] int pageSize = 10,
[SortColumnValidator(typeof(BoardGameDTO))] string? sortColumn = "Name",
[SortOrderValidator] string? sortOrder = "ASC",
string? filterQuery = null)
請(qǐng)注意,我們對(duì) SortColumnValidator 的 EntityType 參數(shù)使用了 BoardGameDTO 而不是 BoardGame 實(shí)體,因此遵循了第 5 章中介紹的單一責(zé)任原則。每當(dāng)我們與客戶交換數(shù)據(jù)時(shí),使用 DTO 而不是實(shí)體類型是一種很好的做法,它將大大提高 Web 應(yīng)用程序的安全狀況。出于這個(gè)原因,我建議始終遵循這種做法,即使它需要額外的工作。
由于 Swashbuckle 中間件的內(nèi)省活動(dòng),模型綁定系統(tǒng)遵循的標(biāo)準(zhǔn)會(huì)自動(dòng)記錄在自動(dòng)生成的 swagger.json 文件中,該文件表示我們 Web API 端點(diǎn)的 OpenAPI 規(guī)范文件(第 3 章)。我們可以通過(guò)執(zhí)行 URL https://localhost:40443/swagger/v1/swagger.json 然后查看文件的 JSON 內(nèi)容來(lái)檢查此類行為。下面是包含 GET /BoardGames 終結(jié)點(diǎn)的前兩個(gè)參數(shù)的摘錄:
{
"name": "PageIndex", ?
"in": "query",
"schema": {
"type": "integer", ?
"format": "int32", ?
"default": 0 ?
}
},
{
"name": "PageSize", ?
"in": "query",
"schema": {
"maximum": 100,
"minimum": 1,
"type": "integer", ?
"format": "int32", ?
"default": 10 ?
}
}
? 參數(shù)名稱
? 參數(shù)類型
? 參數(shù)格式
? 參數(shù)默認(rèn)值
? 參數(shù)名稱
? 參數(shù)類型
? 參數(shù)格式
? 參數(shù)默認(rèn)值
如我們所見(jiàn),關(guān)于我們的參數(shù)的所有值得注意的內(nèi)容都記錄在那里。理想情況下,使用我們 Web API 的客戶端將使用此信息來(lái)創(chuàng)建兼容的用戶界面,該界面可用于以最佳方式與我們的數(shù)據(jù)進(jìn)行交互。一個(gè)很好的例子是 SwaggerUI,它使用 swagger.json 文件來(lái)創(chuàng)建可用于測(cè)試 API 端點(diǎn)的輸入表單。執(zhí)行 URL https://localhost:40443/swagger/index.xhtml,使用右句柄展開(kāi) GET/BoardGames 終結(jié)點(diǎn)面板,然后檢查“參數(shù)”選項(xiàng)卡上的參數(shù)列表(圖 6.1)。
圖 6.1 GET /桌游端點(diǎn)參數(shù)信息
每個(gè)參數(shù)的類型、格式和默認(rèn)值信息都有很好的文檔記錄。如果我們點(diǎn)擊 試用 按鈕,我們可以訪問(wèn)同一輸入表單的編輯模式,我們可以在其中用實(shí)際值填充文本框。如果我們嘗試插入一些明顯無(wú)效的數(shù)據(jù),例如字符串而不是整數(shù),然后單擊“執(zhí)行”按鈕,則 UI 不會(huì)執(zhí)行調(diào)用;相反,它顯示了我們需要糾正的錯(cuò)誤(圖 6.2)。
圖 6.2 包含無(wú)效數(shù)據(jù)的 GET /BoardGames 輸入表單
尚未向 GET /BoardGame 端點(diǎn)發(fā)出任何請(qǐng)求。輸入錯(cuò)誤是由 SwaggerUI 在從 swagger.json 文件中檢索的參數(shù)信息中構(gòu)建的客戶端驗(yàn)證技術(shù)檢測(cè)到的。所有這些都是自動(dòng)發(fā)生的,而無(wú)需我們(幾乎)編寫任何代碼;我們正在充分利用框架的內(nèi)置功能。
我們應(yīng)該依賴客戶端驗(yàn)證嗎?
請(qǐng)務(wù)必了解,SwaggerUI 的客戶端驗(yàn)證功能僅用于改善用戶體驗(yàn)和防止無(wú)用的服務(wù)器往返。這些功能沒(méi)有安全目的,因?yàn)槿魏尉哂凶钌貶TML和/或JavaScript知識(shí)的用戶都可以輕松繞過(guò)它們。
所有客戶端驗(yàn)證控件、規(guī)則和檢查也是如此。它們對(duì)于增強(qiáng)應(yīng)用程序的表示層和阻止無(wú)效請(qǐng)求而不觸發(fā)服務(wù)器端對(duì)應(yīng)項(xiàng)非常有用,從而提高客戶端應(yīng)用程序的整體性能,但它們無(wú)法確保或保護(hù)數(shù)據(jù)的完整性。因此,我們不能也不應(yīng)該依賴客戶端驗(yàn)證。在本書中,由于我們正在處理一個(gè)Web API,它代表了我們可以想象的任何客戶端-服務(wù)器模型的服務(wù)器端伴侶,因此我們必須始終驗(yàn)證所有輸入數(shù)據(jù),無(wú)論客戶端做什么。
內(nèi)置驗(yàn)證屬性
大多數(shù)內(nèi)置驗(yàn)證屬性都由 Swashbuckle 原生支持,它會(huì)自動(dòng)檢測(cè)它們并將其記錄在 swagger.json 文件中。如果我們現(xiàn)在查看我們的 swagger.json 文件,我們將看到 [Range] 屬性是按以下方式記錄的:
{
"name": "pageSize",
"in": "query",
"schema": {
"maximum": 100, ?
"minimum": 1, ?
"type": "integer",
"format": "int32",
"default": 10
}
}
? 范圍屬性最小值
? 范圍屬性最大值
以下是記錄 [RegularExpression] 屬性的方式:
{
"name": "sortOrder",
"in": "query",
"schema": {
"pattern": "ASC|DESC", ?
"type": "string",
"default": "ASC"
}
}
? 正則表達(dá)式屬性的正則表達(dá)式模式
客戶端還可以使用此有價(jià)值的信息來(lái)實(shí)現(xiàn)其他客戶端驗(yàn)證規(guī)則和功能。
自定義驗(yàn)證屬性
不幸的是,Swashbuckle本身并不支持自定義驗(yàn)證器,這并不奇怪,因?yàn)镾washbuckle不可能知道它們是如何工作的。但是該庫(kù)公開(kāi)了一個(gè)方便的過(guò)濾器管道,該管道與 swagger.json 文件生成過(guò)程掛鉤。此功能允許我們創(chuàng)建自己的過(guò)濾器,將它們添加到管道中,并使用它們來(lái)自定義文件的內(nèi)容。
注意Swashbuckle的過(guò)濾器管道在第11章中進(jìn)行了廣泛的介紹。在本節(jié)中,我通過(guò)介紹 IParameterFilter 接口僅提供此功能的一小部分預(yù)覽,因?yàn)槲覀冃枰鼇?lái)滿足我們當(dāng)前的需求。有關(guān)界面的其他信息,請(qǐng)查看第 11 章和/或以下 URL:http://mng.bz/ydNe。
簡(jiǎn)而言之,如果我們想將自定義驗(yàn)證屬性的信息添加到 swagger.json 文件中,我們需要執(zhí)行以下操作:
讓我們把這個(gè)計(jì)劃付諸實(shí)踐,從 [SortOrderValidator] 屬性開(kāi)始。
添加排序順序篩選器
在 Visual Studio 的“解決方案資源管理器”面板中,創(chuàng)建一個(gè)新的 /Swagger/ 文件夾,右鍵單擊它,然后添加新的 SortOrderFilter.cs類文件。新類必須實(shí)現(xiàn) IParameterFilter 接口及其 Apply 方法,以便向 swagger.json 文件添加合適的 JSON 密鑰,如內(nèi)置驗(yàn)證屬性。下面的清單顯示了我們?nèi)绾巫龅竭@一點(diǎn)。
清單 6.3 排序順序過(guò)濾器
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using MyBGList.Attributes;
namespace MyBGList.Swagger
{
public class SortOrderFilter : IParameterFilter
{
public void Apply(
OpenApiParameter parameter,
ParameterFilterContext context)
{
var attributes = context.ParameterInfo?
.GetCustomAttributes(true)
.OfType<SortOrderValidatorAttribute>(); ?
if (attributes != null)
{
foreach (var attribute in attributes) ?
{
parameter.Schema.Extensions.Add(
"pattern",
new OpenApiString(string.Join("|",
attribute.AllowedValues.Select(v => $"^{v}$")))
);
}
}
}
}
}
? 檢查參數(shù)是否具有屬性
? 如果該屬性存在,則采取相應(yīng)的行動(dòng)
請(qǐng)注意,我們使用“模式”JSON 鍵和正則表達(dá)式模式作為值 — 與 [RegularExpression] 內(nèi)置驗(yàn)證屬性使用的行為相同。我們這樣做是為了促進(jìn)客戶端驗(yàn)證檢查的實(shí)施,假設(shè)客戶端在收到信息時(shí)已經(jīng)能夠提供正則表達(dá)式支持(這恰好與我們的驗(yàn)證要求“兼容”)。我們可以使用不同的鍵和/或值類型,將實(shí)現(xiàn)細(xì)節(jié)留給客戶端。接下來(lái),讓我們?yōu)榈诙€(gè)自定義驗(yàn)證屬性創(chuàng)建另一個(gè)篩選器。
添加排序列篩選器
在 /Swagger/ 文件夾中添加新的 SortColumnFilter.cs 類文件。此類類似于 SortOrderFilter 類,但有一些細(xì)微的區(qū)別:這一次,我們必須檢索 EntityType 屬性的名稱,而不是 AllowedValues 字符串?dāng)?shù)組,這需要一些額外的工作。下面的清單提供了源代碼。
清單 6.4 排序列過(guò)濾器
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using MyBGList.Attributes;
namespace MyBGList.Swagger
{
public class SortColumnFilter : IParameterFilter
{
public void Apply(
OpenApiParameter parameter,
ParameterFilterContext context)
{
var attributes = context.ParameterInfo?
.GetCustomAttributes(true)
.OfType<SortColumnValidatorAttribute>(); ?
if (attributes != null)
{
foreach (var attribute in attributes) ?
{
var pattern = attribute.EntityType
.GetProperties()
.Select(p => p.Name);
parameter.Schema.Extensions.Add(
"pattern",
new OpenApiString(string.Join("|",
pattern.Select(v => $"^{v}$")))
);
}
}
}
}
}
? 檢查參數(shù)是否具有屬性
? 如果該屬性存在,則采取相應(yīng)的行動(dòng)
同樣,我們使用“模式”鍵和正則表達(dá)式模式作為值,因?yàn)榧词故沁@個(gè)驗(yàn)證器也與基于正則表達(dá)式的客戶端驗(yàn)證檢查兼容。現(xiàn)在我們需要將這些過(guò)濾器掛接到程序.cs文件中的 Swashbuckle 中間件,以便在生成 Swagger 文件時(shí)將它們考慮在內(nèi)。
綁定 IParameterFilters
打開(kāi) Program.cs 文件,并在頂部添加與我們新實(shí)現(xiàn)的過(guò)濾器相對(duì)應(yīng)的命名空間:
using MyBGList.Swagger;
向下滾動(dòng)到我們將 Swashbuckle 的 Swagger 生成器中間件添加到管道的行,并按以下方式對(duì)其進(jìn)行更改:
builder.Services.AddSwaggerGen(options => {
options.ParameterFilter<SortColumnFilter>(); ?
options.ParameterFilter<SortOrderFilter>(); ?
});
? 將排序列篩選器添加到篩選器管道
? 將 SortOrderFilter 添加到篩選器管道
現(xiàn)在,我們可以通過(guò)在調(diào)試模式下運(yùn)行項(xiàng)目并查看自動(dòng)生成的 swagger.json 文件來(lái)測(cè)試我們所做的工作,使用與之前相同的 URL (https://localhost:40443/swagger/v1/swagger.json)。如果我們做對(duì)了所有事情,我們應(yīng)該看到 sortOrder 和 sortColumn 參數(shù),其中存在 “pattern” 鍵并根據(jù)驗(yàn)證器的規(guī)則填充:
{
"name": "sortColumn",
"in": "query",
"schema": {
"type": "string",
"default": "Name",
"pattern": "^Id$|^Name$|^Year$" ?
}
},
{
"name": "sortOrder",
"in": "query",
"schema": {
"type": "string",
"default": "ASC",
"pattern": "^ASC$|^DESC$" ?
}
}
? 排序列驗(yàn)證器的正則表達(dá)式模式
? SortOrderValidator的正則表達(dá)式模式
重要的是要了解,實(shí)現(xiàn)自定義驗(yàn)證器可能是一項(xiàng)挑戰(zhàn),而且在時(shí)間和源代碼行方面是一項(xiàng)昂貴的任務(wù)。在大多數(shù)情況下,我們不需要這樣做,因?yàn)閮?nèi)置驗(yàn)證屬性可以滿足我們的所有需求。但是,每當(dāng)我們處理復(fù)雜或可能麻煩的客戶端定義輸入時(shí),能夠創(chuàng)建和記錄它們可以有所作為。
到目前為止,我們一直使用簡(jiǎn)單的類型參數(shù)來(lái)處理我們的操作方法:整數(shù)、字符串、布爾值等。此方法是了解模型綁定和驗(yàn)證屬性如何工作的好方法,并且在處理一小組參數(shù)時(shí)通常是首選方法。但是,使用復(fù)雜類型參數(shù)(如 DTO)可以極大地受益于幾種方案,特別是考慮到 ASP.NET Core 模型綁定系統(tǒng)也可以處理它們。
當(dāng)模型綁定的目標(biāo)為復(fù)雜類型時(shí),每個(gè)類型屬性都被視為綁定和驗(yàn)證的單獨(dú)參數(shù)。復(fù)雜類型的每個(gè)屬性都充當(dāng)一個(gè)簡(jiǎn)單的類型參數(shù),在代碼可擴(kuò)展性和靈活性方面具有很大的好處。我們可以將所有參數(shù)框入單個(gè) DTO 類中,而不是可能很長(zhǎng)的方法參數(shù)列表。了解這些優(yōu)勢(shì)的最好方法是將它們付諸實(shí)踐,將我們當(dāng)前的簡(jiǎn)單類型參數(shù)替換為單個(gè)、全面的復(fù)雜類型。
創(chuàng)建請(qǐng)求DTO類
在 Visual Studio 的“解決方案資源管理器”面板中,右鍵單擊 /DTO/ 文件夾,然后添加新的 RequestDTO.cs類文件。此類將包含我們?cè)?BoardGamesController 的 Get 操作方法中接收的所有客戶端定義的輸入?yún)?shù);我們所要做的就是為每個(gè)屬性創(chuàng)建一個(gè)屬性,如下面的列表所示。
清單 6.5 請(qǐng)求 DTO.cs文件
using MyBGList.Attributes;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace MyBGList.DTO
{
public class RequestDTO
{
[DefaultValue(0)] ?
public int PageIndex { get; set; } = 0;
[DefaultValue(10)] ?
[Range(1, 100)] ?
public int PageSize { get; set; } = 10;
[DefaultValue("Name")] ?
[SortColumnValidator(typeof(BoardGameDTO))] ?
public string? SortColumn { get; set; } = "Name";
[DefaultValue("ASC")] ?
[SortOrderValidator] ?
public string? SortOrder { get; set; } = "ASC";
[DefaultValue(null)] ?
public string? FilterQuery { get; set; } = null;
}
}
? 默認(rèn)值屬性
? 內(nèi)置驗(yàn)證屬性
? 自定義驗(yàn)證屬性
請(qǐng)注意,我們已經(jīng)使用 [DefaultValue] 屬性修飾了每個(gè)屬性。此屬性使 Swagger 生成器中間件能夠在 swagger.json 文件中創(chuàng)建“默認(rèn)”鍵,因?yàn)樗鼘o(wú)法看到我們使用方便的 C# 內(nèi)聯(lián)語(yǔ)法設(shè)置的初始值。幸運(yùn)的是,此屬性受支持,并提供了一個(gè)很好的解決方法。現(xiàn)在我們有了 RequestDTO 類,我們可以使用它來(lái)通過(guò)以下方式替換 BoardGamesController 的 Get 方法的簡(jiǎn)單類型參數(shù):
[HttpGet(Name = "GetBoardGames")]
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
public async Task<RestDTO<BoardGame[]>> Get(
[FromQuery] RequestDTO input) ?
{
var query = _context.BoardGames.AsQueryable();
if (!string.IsNullOrEmpty(input.FilterQuery))
query = query.Where(b => b.Name.Contains(input.FilterQuery));
query = query
.OrderBy($"{input.SortColumn} {input.SortOrder}")
.Skip(input.PageIndex * input.PageSize)
.Take(input.PageSize);
return new RestDTO<BoardGame[]>()
{
Data = await query.ToArrayAsync(),
PageIndex = input.PageIndex,
PageSize = input.PageSize,
RecordCount = await _context.BoardGames.CountAsync(),
Links = new List<LinkDTO> {
new LinkDTO(
Url.Action(
null,
"BoardGames",
new { input.PageIndex, input.PageSize },
Request.Scheme)!,
"self",
"GET"),
}
};
}
? 新的復(fù)雜類型參數(shù)
在此代碼中,我們使用 [FromQuery] 屬性告訴路由中間件我們希望從查詢字符串中獲取輸入值,從而保留以前的行為。但是我們可以使用任何其他可用屬性:
能夠使用基于屬性的方法在參數(shù)綁定技術(shù)之間切換是框架的另一個(gè)方便功能。稍后我們將使用其中一些屬性。
我們必須顯式使用 [FromQuery] 屬性,因?yàn)閺?fù)雜類型參數(shù)的默認(rèn)方法是從請(qǐng)求正文中獲取值。我們還必須將源代碼中所有參數(shù)的引用替換為新類的屬性。現(xiàn)在,“新”實(shí)現(xiàn)看起來(lái)比前一個(gè)更時(shí)尚和DRY(不要重復(fù)自己原則)。此外,我們有一個(gè)靈活的通用 DTO 類,可用于在 BoardGamesController 以及我們將來(lái)要添加的其他控制器中實(shí)現(xiàn)類似的基于 GET 的操作方法:DomainsController、MechanicsControllers 等。右?
嗯,沒(méi)有。如果我們更好地查看當(dāng)前的 RequestDTO 類,我們會(huì)發(fā)現(xiàn)它根本不是通用的。問(wèn)題出在 [SortColumnValidator] 屬性中,該屬性需要一個(gè)類型參數(shù)。通過(guò)查看源代碼,我們可以看到,此參數(shù)被硬編碼為 BoardGameDTO 類型:
[SortColumnValidator(typeof(BoardGameDTO))]
我們?nèi)绾谓鉀Q這個(gè)問(wèn)題?乍一想到,我們可能會(huì)想動(dòng)態(tài)傳遞該參數(shù),也許使用泛型 <T> 類型。此方法需要將 RequestDTO 的類聲明更改為
public class RequestDTO<T>
這將允許我們通過(guò)以下方式在操作方法中使用它:
[FromQuery] RequestDTO<BoardGameDTO> input
然后我們將以這種方式更改驗(yàn)證器:
[SortColumnValidator(typeof(T))]
不幸的是,這種方法行不通。在 C# 中,修飾類的屬性在編譯時(shí)計(jì)算,但泛型 <T> 類在運(yùn)行時(shí)之前不會(huì)收到其最終類型信息。原因很簡(jiǎn)單:由于某些屬性可能會(huì)影響編譯過(guò)程,因此編譯器必須能夠在編譯時(shí)完整地定義它們。因此,屬性不能使用泛型類型參數(shù)。
C 語(yǔ)言中的泛型屬性類型限制#
根據(jù) Eric Lippert(前Microsoft工程師和 C# 語(yǔ)言設(shè)計(jì)團(tuán)隊(duì)成員)的說(shuō)法,添加此限制是為了降低語(yǔ)言和編譯器代碼的復(fù)雜性,以應(yīng)對(duì)不會(huì)增加太多價(jià)值的用例。他的解釋(釋義)可以在Jon Skeet給出的StackOverflow答案中找到:http://mng.bz/Mlw8。
有關(guān)本主題的其他信息,請(qǐng)查看 http://mng.bz/Jl0K 上的 C# 泛型Microsoft指南。
此行為將來(lái)可能會(huì)更改,因?yàn)?.NET 社區(qū)經(jīng)常要求 C# 語(yǔ)言設(shè)計(jì)團(tuán)隊(duì)重新評(píng)估它。
如果屬性不能使用泛型類型,我們?nèi)绾谓鉀Q這個(gè)問(wèn)題?答案不難猜:如果山不去穆罕默德,穆罕默德必須去山上。換句話說(shuō),我們需要將屬性方法替換為 ASP.NET 框架支持的另一種驗(yàn)證技術(shù)。幸運(yùn)的是,這樣的技術(shù)恰好存在,它的名字是IValidatableObject。
實(shí)現(xiàn) IValidatableObject
IValidatableObject 接口提供了一種驗(yàn)證類的替代方法。它的工作方式類似于類級(jí)屬性,這意味著我們可以使用它來(lái)驗(yàn)證任何 DTO 類型,而不管其屬性如何,以及它包含的所有屬性級(jí)驗(yàn)證屬性。與驗(yàn)證屬性相比,IValidatableObject 接口有兩個(gè)主要優(yōu)點(diǎn):
讓我們使用 IValidatableObject 接口在當(dāng)前的 RequestDTO 類中實(shí)現(xiàn)排序列驗(yàn)證檢查。以下是我們需要做的:
下面的清單顯示了我們?nèi)绾螌?shí)現(xiàn)這些步驟。
清單 6.6 請(qǐng)求DTO.cs文件(版本2)
using MyBGList.Attributes;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace MyBGList.DTO
{
public class RequestDTO<T> : IValidatableObject ?
{
[DefaultValue(0)]
public int PageIndex { get; set; } = 0;
[DefaultValue(10)]
[Range(1, 100)]
public int PageSize { get; set; } = 10;
[DefaultValue("Name")]
public string? SortColumn { get; set; } = "Name";
[SortOrderValidator]
[DefaultValue("ASC")]
public string? SortOrder { get; set; } = "ASC";
[DefaultValue(null)]
public string? FilterQuery { get; set; } = null;
public IEnumerable<ValidationResult> Validate( ?
ValidationContext validationContext)
{
var validator = new SortColumnValidatorAttribute(typeof(T));
var result = validator
.GetValidationResult(SortColumn, validationContext);
return (result != null)
? new [] { result }
: new ValidationResult[0];
}
}
}
? 泛型類型和 IValidatableObject 接口
? 驗(yàn)證方法實(shí)現(xiàn)
在此代碼中,我們看到 Validate 方法實(shí)現(xiàn)呈現(xiàn)了一個(gè)情節(jié)轉(zhuǎn)折:我們?cè)诤笈_(tái)使用 SortColumnValidator!主要區(qū)別在于,這一次,我們將其用作“標(biāo)準(zhǔn)”類實(shí)例,而不是數(shù)據(jù)注釋屬性,這允許我們將泛型類型作為參數(shù)傳遞。
這幾乎感覺(jué)像作弊,對(duì)吧?但事實(shí)并非如此;我們正在回收我們已經(jīng)做過(guò)的事情。我們可以做到這一點(diǎn),這要?dú)w功于 ValidationAttribute 基類公開(kāi)的 GetValidationResult 方法被定義為公共的,這允許我們創(chuàng)建驗(yàn)證器的實(shí)例并調(diào)用它來(lái)驗(yàn)證 SortColumn 屬性。
現(xiàn)在我們有一個(gè)要在代碼中使用的泛型 DTO 類,請(qǐng)打開(kāi) BoardGamesControllers.cs 文件,向下滾動(dòng)到 Get 方法,并按以下方式更新其簽名:
public async Task<RestDTO<BoardGame[]>> Get(
[FromQuery] RequestDTO<BoardGameDTO> input)
該方法的其余代碼不需要任何更改。我們已將 BoardGameDTO 指定為泛型類型參數(shù),以便 RequestDTO 的 Validate 方法將根據(jù) SortColumn 輸入數(shù)據(jù)檢查其屬性,確保客戶端設(shè)置的用于對(duì)數(shù)據(jù)進(jìn)行排序的列對(duì)該特定請(qǐng)求有效。
添加域控制器和機(jī)械控制器
現(xiàn)在是創(chuàng)建DomainsController和MechanicsController的好時(shí)機(jī),復(fù)制我們迄今為止在BoardGamesController中實(shí)現(xiàn)的所有功能。這樣做將允許我們對(duì)通用的 RequestDTO 類和我們的 IValidatableObject 靈活實(shí)現(xiàn)進(jìn)行適當(dāng)?shù)臏y(cè)試。我們還需要添加幾個(gè)新的DTO,DomainDTO類和MechanicDTO,它們將與BoardGameDTO類類似。
由于篇幅原因,我在這里不列出這四個(gè)文件的源代碼。該代碼位于本書 GitHub 存儲(chǔ)庫(kù)的 /Chapter_06/ 文件夾中的 /Controllers/ 和 /DTO/ 子文件夾中。我強(qiáng)烈建議您嘗試在不查看 GitHub 文件的情況下實(shí)現(xiàn)它們,因?yàn)檫@是練習(xí)您到目前為止所學(xué)的所有內(nèi)容的好機(jī)會(huì)。
測(cè)試新控制器
當(dāng)新控制器準(zhǔn)備就緒時(shí),我們可以使用以下 URL 端點(diǎn)(對(duì)于 GET 方法)徹底檢查它們,
以及 SwaggerUI(用于 POST 和 DELETE 方法)。
注意每當(dāng)我們刪除域或機(jī)制時(shí),我們?cè)诘?4 章中為這些實(shí)體設(shè)置的級(jí)聯(lián)規(guī)則也會(huì)刪除其對(duì)相應(yīng)多對(duì)多查找表的所有引用。所有棋盤游戲都將失去與該特定領(lǐng)域或機(jī)制的關(guān)系(如果有的話)。要恢復(fù),我們需要?jiǎng)h除所有棋盤游戲,然后使用 SeedController 的 Put 方法重新加載它們。
更新 IParameterFilters
在我們進(jìn)一步討論之前,我們需要做最后一件事。現(xiàn)在我們已經(jīng)用 DTO 替換了簡(jiǎn)單的類型參數(shù),SortOrderFilter 和 SortColumnFilter 將無(wú)法再找到我們的自定義驗(yàn)證器。原因很簡(jiǎn)單:他們當(dāng)前的實(shí)現(xiàn)是使用上下文的 GetCustomAttributes 方法查找它們。ParameterInfo 對(duì)象,它返回應(yīng)用于篩選器處理的參數(shù)的屬性數(shù)組。現(xiàn)在,此 ParameterInfo 包含 DTO 本身的引用,這意味著前面的方法將返回應(yīng)用于整個(gè) DTO 類的屬性,而不是其屬性。
為了解決這個(gè)問(wèn)題,我們需要擴(kuò)展屬性查找行為,以便它還檢查分配給給定參數(shù)屬性的屬性(如果有)。以下是我們?nèi)绾胃?SortOrderFilter 的源代碼來(lái)執(zhí)行此操作:
var attributes = context.ParameterInfo
.GetCustomAttributes(true)
.Union( ?
context.ParameterInfo.ParameterType.GetProperties()
.Where(p => p.Name == parameter.Name)
.SelectMany(p => p.GetCustomAttributes(true))
)
.OfType<SortOrderValidatorAttribute>();
? 檢索參數(shù)的屬性自定義屬性
請(qǐng)注意,我們使用聯(lián)合 LINQ 擴(kuò)展方法來(lái)生成單個(gè)數(shù)組,其中包含分配給 ParameterInfo 對(duì)象本身的自定義屬性,以及分配給該 ParameterInfo 對(duì)象的屬性的屬性以及篩選器當(dāng)前正在處理的參數(shù)的名稱(如果有)。由于這種新的實(shí)現(xiàn),我們的過(guò)濾器將能夠找到分配給任何復(fù)雜類型參數(shù)的屬性以及簡(jiǎn)單類型參數(shù)的自定義屬性,從而確保完全向后兼容。
SortOrderFilter 已修復(fù),但 SortColumnFilter 呢?不幸的是,修復(fù)并不是那么簡(jiǎn)單。[SortColumnValidator] 屬性不應(yīng)用于任何屬性,因此 SortColumnFilter 無(wú)法找到它。我們可能會(huì)認(rèn)為,通過(guò)將屬性添加到 IValidatableObject 的 Validate 方法,然后調(diào)整篩選器的查找行為以包含屬性以外的方法,我們可以解決此問(wèn)題。但我們已經(jīng)知道此解決方法將失敗;該屬性仍需要無(wú)法在編譯時(shí)設(shè)置的泛型類型參數(shù)。由于空間原因,我們現(xiàn)在不會(huì)解決這個(gè)問(wèn)題;我們將把這個(gè)任務(wù)推遲到第11章,屆時(shí)我們將學(xué)習(xí)其他涉及Swagger和Swashbuckle的API文檔技術(shù)。
提示在繼續(xù)操作之前,請(qǐng)確保將前面的修補(bǔ)程序也應(yīng)用于排序列篩選器。要添加的源代碼是相同的,因?yàn)閮蓚€(gè)篩選器使用相同的查找策略。這個(gè)補(bǔ)丁似乎毫無(wú)用處,因?yàn)?SortColumnFilter 不起作用(并且在一段時(shí)間內(nèi)不起作用),但讓我們的類保持最新是一種很好的做法,即使我們沒(méi)有積極使用或指望它們。
我們的數(shù)據(jù)驗(yàn)證之旅已經(jīng)結(jié)束,至少目前是這樣。在下一節(jié)中,我們將學(xué)習(xí)如何處理驗(yàn)證錯(cuò)誤和程序異常。
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了幾個(gè)服務(wù)器端數(shù)據(jù)驗(yàn)證檢查,除了通常在公然無(wú)效的 HTTP 請(qǐng)求的情況下提供的場(chǎng)景之外,我們還為 Web API 創(chuàng)建了許多其他失敗場(chǎng)景。根據(jù)確定模型綁定失敗的驗(yàn)證規(guī)則并受其限制,每條缺失、格式錯(cuò)誤、不正確或其他無(wú)效的輸入數(shù)據(jù)都將被我們的 Web API 拒絕,并顯示 HTTP 400 - 錯(cuò)誤請(qǐng)求錯(cuò)誤響應(yīng)。在本章開(kāi)頭,當(dāng)我們嘗試將字符串值傳遞給 pageIndex 參數(shù)而不是數(shù)字 1 時(shí),我們遇到了這種行為。但是,HTTP 400狀態(tài)并不是來(lái)自服務(wù)器的唯一響應(yīng)。我們還得到了一個(gè)有趣的響應(yīng)正文,值得再看一看:
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title":"One or more validation errors occurred.",
"status":400,
"traceId":"00-a074ebace7131af6561251496331fc65-ef1c633577161417-00",
"errors":{
"pageIndex":["The value 'string' is not valid."]
}
}
正如我們所看到的,我們的 Web API 不僅告訴客戶端出了問(wèn)題;它還提供有關(guān)錯(cuò)誤的上下文信息,包括帶有拒絕值的參數(shù),使用 HTTP API 響應(yīng)格式標(biāo)準(zhǔn)在 https://tools.ietf.org/html/rfc7807 中定義,該標(biāo)準(zhǔn)在第 2 章中簡(jiǎn)要介紹。所有這些工作都由引擎蓋下的框架自動(dòng)執(zhí)行;我們不需要做任何事情。這項(xiàng)工作是 [ApiController] 屬性的內(nèi)置功能,用于裝飾我們的控制器。
若要了解 [ApiController] 屬性為我們做了什么,我們需要退后一步,查看整個(gè)模型綁定和驗(yàn)證系統(tǒng)生命周期。圖 6.3 說(shuō)明了框架在典型 HTTP 請(qǐng)求中執(zhí)行的各種步驟的流程。
圖 6.3 具有 [ApiController] 屬性的模型綁定和驗(yàn)證生命周期
我們的興趣點(diǎn)在 HTTP 請(qǐng)求到達(dá)后立即開(kāi)始,路由中間件調(diào)用模型綁定系統(tǒng),該系統(tǒng)按順序執(zhí)行兩個(gè)相關(guān)任務(wù):
重要的教訓(xùn)是,綁定錯(cuò)誤和驗(yàn)證錯(cuò)誤都由框架處理(使用 HTTP 400 錯(cuò)誤響應(yīng)),甚至無(wú)需調(diào)用操作方法。換句話說(shuō),[ApiController] 屬性提供了一個(gè)完全自動(dòng)化的錯(cuò)誤處理管理系統(tǒng)。如果我們沒(méi)有特定的要求,這種方法可能很棒,但是如果我們想自定義某些東西怎么辦?在以下部分中,我們將了解如何執(zhí)行此操作。
我們可能要做的最重要的事情是定義一些自定義錯(cuò)誤消息而不是默認(rèn)錯(cuò)誤消息。讓我們從模型綁定錯(cuò)誤開(kāi)始。
自定義模型綁定錯(cuò)誤
要更改默認(rèn)的模型綁定錯(cuò)誤消息,我們需要修改 ModelBindingMessageProvider 的設(shè)置,可以從 ControllersMiddleware 的配置選項(xiàng)訪問(wèn)該設(shè)置。打開(kāi)程序.cs文件,找到構(gòu)建器。Services.AddControllers 方法,并按以下方式替換當(dāng)前的無(wú)參數(shù)實(shí)現(xiàn)(粗體換行):
builder.Services.AddControllers(options => {
options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(
(x) => $"The value '{x}' is invalid.");
options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor(
(x) => $"The field {x} must be a number.");
options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor(
(x, y) => $"The value '{x}' is not valid for {y}.");
options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(
() => $"A value is required.");
});
為簡(jiǎn)單起見(jiàn),此示例僅更改許多可用消息中的三個(gè)。
自定義模型驗(yàn)證錯(cuò)誤
更改模型驗(yàn)證錯(cuò)誤消息很容易,因?yàn)?ValidationAttribute 基類附帶了一個(gè)方便的 ErrorMessage 屬性,可用于此目的。我們?cè)趯?shí)現(xiàn)自己的自定義驗(yàn)證器時(shí)使用了它。相同的技術(shù)可用于所有內(nèi)置驗(yàn)證器:
[Required(ErrorMessage = "This value is required.")]
[Range(1, 100, ErrorMessage = "The value must be between 1 and 100.")]
但是,通過(guò)這樣做,我們將自定義錯(cuò)誤消息,而不是 ModelState 驗(yàn)證過(guò)程本身,該過(guò)程仍由框架自動(dòng)執(zhí)行。
假設(shè)我們希望(或被要求)將當(dāng)前的 HTTP 400 - 錯(cuò)誤請(qǐng)求替換為不同的狀態(tài)代碼,以防某些特定的驗(yàn)證失敗,例如不正確的 pageSize 的 HTTP 501 - 未實(shí)現(xiàn)狀態(tài)代碼整數(shù)值(小于 1 或大于 100)。除非我們找到一種方法在操作方法中手動(dòng)檢查 ModelState(并相應(yīng)地采取行動(dòng)),否則無(wú)法處理此更改請(qǐng)求。但我們知道我們不能這樣做,因?yàn)橛捎?[ApiController] 功能,ModelState 驗(yàn)證和錯(cuò)誤處理過(guò)程由框架自動(dòng)處理。如果 ModelState 無(wú)效,操作方法甚至不會(huì)發(fā)揮作用;將改為返回默認(rèn)(和不需要的)HTTP 400 錯(cuò)誤。
可能想到的第一個(gè)解決方案是擺脫 [ApiController] 屬性,這將刪除自動(dòng)行為并允許我們手動(dòng)檢查 ModelState,即使它無(wú)效。這種方法行得通嗎?會(huì)的。圖 6.4 顯示了模型綁定和驗(yàn)證生命周期圖如何在沒(méi)有 [ApiController] 屬性的情況下工作。
圖 6.4 沒(méi)有 [ApiController] 屬性的模型綁定和驗(yàn)證生命周期
正如我們所看到的,現(xiàn)在無(wú)論 ModelState 狀態(tài)如何,都將執(zhí)行操作方法,從而允許我們檢查它,查看出了什么問(wèn)題,并采取相應(yīng)的行動(dòng),這正是我們想要的。但是我們不應(yīng)該承諾如此苛刻的解決方法,因?yàn)?[ApiController] 屬性為我們的控制器提供了我們可能想要保留的其他幾個(gè)功能。相反,我們應(yīng)該禁用自動(dòng)模型狀態(tài)驗(yàn)證功能,這可以通過(guò)調(diào)整 [ApiController] 屬性本身的默認(rèn)配置設(shè)置來(lái)實(shí)現(xiàn)。
配置 API 控制器的行為
打開(kāi) Program.cs 文件,找到我們實(shí)例化應(yīng)用程序局部變量的行:
var app = builder.Build();
將以下代碼行放在其正上方:
builder.Services.Configure<ApiBehaviorOptions>(options =>
options.SuppressModelStateInvalidFilter = true);
var app = builder.Build();
此設(shè)置禁止在模型狀態(tài)無(wú)效時(shí)自動(dòng)返回 BadRequestObjectResult 的篩選器。現(xiàn)在,我們可以在不刪除 [ApiController] 屬性的情況下實(shí)現(xiàn)所有控制器的預(yù)期,并且我們已準(zhǔn)備好通過(guò)有條件地返回 HTTP 501 狀態(tài)代碼來(lái)實(shí)現(xiàn)更改請(qǐng)求。
實(shí)現(xiàn)自定義 HTTP 狀態(tài)代碼
為簡(jiǎn)單起見(jiàn),假設(shè)更改請(qǐng)求僅影響域控制器。打開(kāi) /Controllers/DomainsController.cs 文件,然后向下滾動(dòng)到 Get 操作方法。以下是我們需要做的:
以下是我們?nèi)绾螌?shí)現(xiàn)它:
[HttpGet(Name = "GetDomains")]
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
public async Task<ActionResult<RestDTO<Domain[]>>> Get( ?
[FromQuery] RequestDTO<DomainDTO> input)
{
if (!ModelState.IsValid) ?
{
var details = new ValidationProblemDetails(ModelState);
details.Extensions["traceId"] =
System.Diagnostics.Activity.Current?.Id
?? HttpContext.TraceIdentifier;
if (ModelState.Keys.Any(k => k == "PageSize"))
{
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.6.2";
details.Status = StatusCodes.Status501NotImplemented;
return new ObjectResult(details) {
StatusCode = StatusCodes.Status501NotImplemented
};
}
else
{
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.5.1";
details.Status = StatusCodes.Status400BadRequest;
return new BadRequestObjectResult(details);
}
}
// ... code omitted ... ?
}
? 新的返回值(操作結(jié)果<T>)
? 模型狀態(tài)無(wú)效時(shí)要執(zhí)行的步驟
? 由于空格原因省略了代碼(不變)
我們可以通過(guò)使用返回 true 或 false 的 IsValid 屬性輕松檢查 ModelState 狀態(tài)。如果我們確定 ModelState 無(wú)效,我們會(huì)檢查錯(cuò)誤集合中是否存在“PageSize”鍵,并創(chuàng)建一個(gè) UnprocessableEntity 或 BadRequest 結(jié)果以返回到客戶端。該實(shí)現(xiàn)需要幾行代碼,因?yàn)槲覀兿M麡?gòu)建一個(gè)記錄錯(cuò)誤詳細(xì)信息的豐富請(qǐng)求正文,包括對(duì)記錄錯(cuò)誤狀態(tài)代碼的 RFC 的引用、traceId 等。
這種方法迫使我們將操作方法的返回類型從 Task<RestDTO<Domain[]>>更改為 Task<ActionResult<RestDTO<Domain[]>>>,因?yàn)楝F(xiàn)在我們需要處理兩種不同類型的響應(yīng):如果 ModelState 驗(yàn)證失敗,則為 ObjectResult,如果成功,則為 JSON 對(duì)象。ActionResult 是一個(gè)不錯(cuò)的選擇,因?yàn)橛捎谄浞盒皖愋偷闹С郑梢蕴幚磉@兩種類型。
現(xiàn)在,我們可以測(cè)試 DomainsController 的 Get 操作方法的新行為。此 URL 應(yīng)返回 HTTP 501 狀態(tài)代碼:https://localhost:40443/Domains?pageSize=101。這個(gè)應(yīng)該用HTTP 400狀態(tài)代碼響應(yīng):https://localhost:40443/Domains?sortOrder=InvalidValue。
由于我們還需要檢查 HTTP 狀態(tài)代碼,而不僅僅是響應(yīng)正文,因此請(qǐng)務(wù)必在執(zhí)行網(wǎng)址之前打開(kāi)瀏覽器的“網(wǎng)絡(luò)”標(biāo)簽頁(yè)(可在所有基于 Chrome 的瀏覽器中通過(guò)開(kāi)發(fā)者工具訪問(wèn)),這是一種實(shí)時(shí)查看每個(gè) HTTP 響應(yīng)狀態(tài)代碼的快速有效方法。
意外的回歸錯(cuò)誤
到目前為止,一切都很好 - 除了我們?cè)谒锌刂破髦袩o(wú)意中造成的非平凡回歸錯(cuò)誤!要了解我在說(shuō)什么,請(qǐng)嘗試針對(duì) BoardGamesController 的 Get 方法執(zhí)行上一節(jié)中的兩個(gè)“無(wú)效”URL:
第一個(gè) URL 返回 101 個(gè)棋盤游戲,第二個(gè) URL 由于動(dòng)態(tài) LINQ 中的語(yǔ)法錯(cuò)誤而引發(fā)未經(jīng)處理的異常。我們的驗(yàn)證器怎么了?
答案應(yīng)該是顯而易見(jiàn)的:它們?nèi)匀挥行В捎谖覀兘昧?[ApiController] 的自動(dòng) ModelState 驗(yàn)證功能(和 HTTP 400 響應(yīng)),因此即使某些輸入?yún)?shù)無(wú)效,也會(huì)執(zhí)行所有操作方法,除了 DomainsController 的 Get 操作方法外,無(wú)需手動(dòng)驗(yàn)證來(lái)填補(bǔ)空白!我們的 BoardGamesController 和 MechanicsController,以及除 Get 之外的所有 DomainsController 操作方法,不再受到不安全輸入的保護(hù)。不過(guò),不要驚慌;我們可以解決問(wèn)題。
同樣,我們可能會(huì)想從 DomainsController 中刪除 [ApiController] 屬性,并解決我們的回歸錯(cuò)誤,而不會(huì)進(jìn)一步麻煩。不幸的是,這種方法不起作用;它將防止錯(cuò)誤影響其他控制器,但不能解決域控制器的其他操作方法的問(wèn)題。此外,我們將失去[ApiController]的其他有用功能,這就是為什么我們一開(kāi)始沒(méi)有擺脫它的原因。
想想我們做了什么:為整個(gè) Web 應(yīng)用程序禁用了 [ApiController] 的一個(gè)功能,因?yàn)槲覀儾幌M鼮閱蝹€(gè)控制器的操作方法觸發(fā)。這就是錯(cuò)誤。這個(gè)想法很好;我們需要縮小范圍。
實(shí)現(xiàn) IActionModelConvention 篩選器
我們可以通過(guò)使用方便的 ASP.NET Core 過(guò)濾器管道來(lái)獲取我們想要的東西,它允許我們自定義 HTTP 請(qǐng)求/響應(yīng)生命周期的行為。我們將創(chuàng)建一個(gè)篩選器屬性,用于檢查給定操作方法中是否存在 ModelStateInvalidFilter 并將其刪除。此設(shè)置將具有與我們放置在 Program.cs 文件中的配置設(shè)置相同的效果,但僅針對(duì)我們將選擇使用該 filter 屬性修飾的操作方法。換句話說(shuō),我們將能夠有條件地禁用 ModelState 自動(dòng)驗(yàn)證功能(選擇退出、默認(rèn)加入),而不必為所有人關(guān)閉它(默認(rèn)退出)。
讓我們把這個(gè)理論付諸實(shí)踐。在 /Attributes/ 文件夾中創(chuàng)建一個(gè)新的 ManualValidationFilterAttribute.cs 類文件,并用下面的清單中的源代碼填充它。
清單 6.7 手動(dòng)驗(yàn)證過(guò)濾器屬性
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace MyBGList.Attributes
{
public class ManualValidationFilterAttribute
: Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
if (action.Filters[i] is ModelStateInvalidFilter
|| action.Filters[i].GetType().Name ==
"ModelStateInvalidFilterFactory")
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
}
遺憾的是,ModelStateInvalidFilterFactory 類型被標(biāo)記為內(nèi)部,這使我們無(wú)法使用強(qiáng)類型方法檢查篩選器是否存在。我們必須將 Name 屬性與類的文字名稱進(jìn)行比較。這種方法并不理想,如果名稱在框架的未來(lái)版本中發(fā)生更改,則可能會(huì)停止工作,但就目前而言,它將解決問(wèn)題。現(xiàn)在我們有了過(guò)濾器,我們需要像任何其他屬性一樣將其應(yīng)用于 DomainsController 的 Get 操作方法:
[HttpGet(Name = "GetDomains")]
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
[ManualValidationFilter] ?
public async Task<ActionResult<RestDTO<Domain[]>>> Get(
[FromQuery] RequestDTO<DomainDTO> input)
? 新的手動(dòng)驗(yàn)證過(guò)濾器屬性
現(xiàn)在我們可以刪除(或注釋掉)導(dǎo)致程序.cs文件中回歸錯(cuò)誤的應(yīng)用程序范圍的設(shè)置:
// Code replaced by the [ManualValidationFilter] attribute
// builder.Services.Configure<ApiBehaviorOptions>(options =>
// options.SuppressModelStateInvalidFilter = true);
我們?yōu)樗锌刂破骱头椒ㄖ匦聠⒂昧俗詣?dòng) ModelState 驗(yàn)證功能,僅將已實(shí)現(xiàn)合適回退的單個(gè)操作方法保留為手動(dòng)狀態(tài)。我們已經(jīng)找到了一種方法來(lái)滿足我們的更改請(qǐng)求,同時(shí)修復(fù)我們意想不到的錯(cuò)誤 - 并且不放棄任何東西。此外,我們?cè)谶@里所做的一切都幫助我們獲得了經(jīng)驗(yàn),提高了我們對(duì) ASP.NET Core 請(qǐng)求/響應(yīng)管道以及底層模型綁定和驗(yàn)證機(jī)制的認(rèn)識(shí)。我們的手動(dòng)模型狀態(tài)驗(yàn)證概述已經(jīng)結(jié)束,至少目前是這樣。
ModelState 對(duì)象并不是我們可能想要處理的應(yīng)用程序錯(cuò)誤的唯一來(lái)源。我們?cè)谑褂?Web API 時(shí)遇到的大多數(shù)應(yīng)用程序錯(cuò)誤不是由于客戶端定義的輸入數(shù)據(jù)造成的,而是由于源代碼的意外行為造成的:空引用異常、DBMS 連接失敗、數(shù)據(jù)檢索錯(cuò)誤、堆棧溢出等。所有這些問(wèn)題都可能會(huì)引發(fā)異常,這些異常(正如我們從第2章開(kāi)始知道的那樣)將由DeveloperExceptionPageMiddleware(如果相應(yīng)的應(yīng)用程序設(shè)置為true)和ExceptionHandlingMiddleware(如果設(shè)置為false)捕獲并處理。
在第 2 章中,當(dāng)我們實(shí)現(xiàn) UseDeveloperExceptionPage 應(yīng)用程序設(shè)置時(shí),我們?cè)谕ㄓ?appsettings.json 文件中將其設(shè)置為 false,在 appsettings 中將其設(shè)置為 true。開(kāi)發(fā).json 文件。我們使用此方法來(lái)確保僅在開(kāi)發(fā)環(huán)境中執(zhí)行應(yīng)用時(shí)才使用 DeveloperExceptionPageMiddleware。此行為在程序.cs文件的代碼部分中清晰可見(jiàn),我們將 ExceptionHandling 中間件添加到管道中:
if (app.Configuration.GetValue<bool>("UseDeveloperExceptionPage"))
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/error");
讓我們暫時(shí)禁用此開(kāi)發(fā)覆蓋,以便我們可以專注于 Web API 在處理實(shí)際客戶端(換句話說(shuō),在生產(chǎn)中)時(shí)如何處理異常。打開(kāi) appSettings.Development.json 文件,并將 UseDeveloperExceptionPage 設(shè)置的值從 true 更改為 false:
"UseDeveloperExceptionPage": false
現(xiàn)在,即使在開(kāi)發(fā)中,我們的應(yīng)用程序也將采用生產(chǎn)錯(cuò)誤處理行為,允許我們?cè)诟滤鼤r(shí)檢查我們正在做什么。在第 2 章中,我們將 ExceptionHandlingMiddleware 的錯(cuò)誤處理路徑設(shè)置為 “/error” 端點(diǎn),我們?cè)诔绦?cs文件中使用以下最小 API MapGet 方法實(shí)現(xiàn)了該端點(diǎn):
app.MapGet("/error",
[EnableCors("AnyOrigin")]
[ResponseCache(NoStore = true)] () =>
Results.Problem());
我們當(dāng)前的實(shí)現(xiàn)由一行代碼組成,該代碼返回一個(gè) ProblemDetails 對(duì)象,從而生成符合 RFC 7807 的 JSON 響應(yīng)。我們?cè)诘?2 章中通過(guò)實(shí)現(xiàn)和執(zhí)行 /error/test 端點(diǎn)(引發(fā)異常)測(cè)試了此行為。讓我們?cè)俅螆?zhí)行它以再次查看它:
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500
}
這種簡(jiǎn)單而有效的響應(yīng)清楚地表明,我們已經(jīng)為生產(chǎn)環(huán)境設(shè)置了一個(gè)不錯(cuò)的異常處理策略。每次出現(xiàn)問(wèn)題時(shí),或者當(dāng)我們想要手動(dòng)在代碼中引發(fā)異常時(shí),我們可以確定調(diào)用客戶端將收到 HTTP 500 錯(cuò)誤狀態(tài)代碼以及標(biāo)準(zhǔn)(且符合 RFC 7807)響應(yīng)正文。
同時(shí),我們可以看到整體結(jié)果沒(méi)有信息量。我們僅通過(guò)返回 HTTP 500 狀態(tài)代碼和以人類可讀形式解釋錯(cuò)誤的最小響應(yīng)正文來(lái)告訴客戶端出了問(wèn)題。
我們面臨的場(chǎng)景與 [ApiController] 的 ModelState 驗(yàn)證所經(jīng)歷的情況相同,這是一種自動(dòng)行為,對(duì)于大多數(shù)方案來(lái)說(shuō)可能很方便,但如果我們需要進(jìn)一步自定義它,可能會(huì)受到限制。我們可能需要返回不同的狀態(tài)代碼,具體取決于引發(fā)的異常。或者,我們可能希望在某處記錄錯(cuò)誤和/或向某人發(fā)送電子郵件通知(取決于異常類型和/或上下文)。
幸運(yùn)的是,ExceptionHandlingMiddleware可以配置為執(zhí)行所有這些操作,甚至更多,只需相對(duì)較少的代碼行。在下面的部分中,我們將更好地研究 ExceptionHandlingMiddleware(畢竟,我們只是在第 2 章中觸及了它的表面),并了解如何充分利用它。
使用異常處理中間件
自定義當(dāng)前行為的第一件事是為 ProblemDetails 對(duì)象提供有關(guān)異常的一些其他詳細(xì)信息,例如其 Message 屬性值。為此,我們需要檢索兩個(gè)對(duì)象:
以下是我們?nèi)绾巫龅竭@一點(diǎn)(相關(guān)代碼以粗體顯示):
app.MapGet("/error",
[EnableCors("AnyOrigin")]
[ResponseCache(NoStore = true)] (HttpContext context) => ?
{
var exceptionHandler =
context.Features.Get<IExceptionHandlerPathFeature>(); ?
// TODO: logging, sending notifications, and more ?
var details = new ProblemDetails();
details.Detail = exceptionHandler?.Error.Message; ?
details.Extensions["traceId"] =
System.Diagnostics.Activity.Current?.Id
?? context.TraceIdentifier;
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.6.1";
details.Status = StatusCodes.Status500InternalServerError;
return Results.Problem(details);
});
? 添加 HttpContext
? 檢索異常處理程序
? 執(zhí)行其他與錯(cuò)誤相關(guān)的管理任務(wù)
? 設(shè)置異常消息
執(zhí)行此升級(jí)后,我們可以啟動(dòng)應(yīng)用并執(zhí)行 /error/test 終結(jié)點(diǎn)以獲取更詳細(xì)的響應(yīng)正文:
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,
"detail":"test", ?
"traceId":"00-7cfd2605a885fbaed6a2abf0bc59944e-28bf94ef8a8c80a7-00" ?
}
? New JSON data
請(qǐng)注意,我們正在手動(dòng)實(shí)例化 ProblemDetails 對(duì)象實(shí)例,根據(jù)需要對(duì)其進(jìn)行配置,然后將其傳遞給 Results.Problem 方法重載,該方法重載將其作為參數(shù)接受。但是,我們可以做的不僅僅是配置 ProblemDetails 對(duì)象的屬性。我們還可以執(zhí)行以下操作:
其中一些可能性將在后面的章節(jié)中介紹。
警告請(qǐng)務(wù)必了解,異常處理中間件將使用原始 HTTP 方法重新執(zhí)行請(qǐng)求。處理程序終結(jié)點(diǎn)(在我們的方案中,處理 /error/ 路徑的 MapGet 方法)不應(yīng)限制為一組有限的 HTTP 方法,因?yàn)樗鼉H適用于它們。如果我們想根據(jù)原始 HTTP 方法以不同的方式處理異常,我們可以將不同的 HTTP 動(dòng)詞屬性應(yīng)用于具有相同名稱的多個(gè)操作。例如,我們可以使用 [HttpGet] 只處理 GET 異常,使用 [HttpPost] 只處理 POST 異常。
異常處理操作
我們可以使用 UseExceptionHandler 方法的重載,而不是將異常處理過(guò)程委托給自定義終結(jié)點(diǎn),該方法接受 Action<IApplicationBuilder> 對(duì)象實(shí)例作為參數(shù)。這種方法允許我們?cè)诓恢付▽S枚它c(diǎn)的情況下獲得相同級(jí)別的自定義。以下是我們?nèi)绾问褂迷撝剌d來(lái)使用我們當(dāng)前在最小 API 的 MapGet 方法中的實(shí)現(xiàn):
app.UseExceptionHandler(action => {
action.Run(async context =>
{
var exceptionHandler =
context.Features.Get<IExceptionHandlerPathFeature>();
var details = new ProblemDetails();
details.Detail = exceptionHandler?.Error.Message;
details.Extensions["traceId"] =
System.Diagnostics.Activity.Current?.Id
?? context.TraceIdentifier;
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.6.1";
details.Status = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsync(
System.Text.Json.JsonSerializer.Serialize(details)); ?
});
});
? JSON 序列化問(wèn)題詳細(xì)信息對(duì)象
正如我們所看到的,源代碼幾乎與MapGet方法的實(shí)現(xiàn)相同。唯一真正的區(qū)別是,在這里,我們需要將響應(yīng)正文直接寫入 HTTP 響應(yīng)緩沖區(qū);我們必須注意將 ProblemDetails 對(duì)象實(shí)例序列化為實(shí)際的 JSON 格式字符串。現(xiàn)在我們已經(jīng)對(duì)框架提供的各種錯(cuò)誤處理方法進(jìn)行了一些實(shí)踐,我們已經(jīng)準(zhǔn)備好將這些知識(shí)應(yīng)用于第 7 章:應(yīng)用程序日志記錄的主題。
是時(shí)候用我們的產(chǎn)品所有者給出的通常的假設(shè)任務(wù)分配列表來(lái)挑戰(zhàn)自己了。練習(xí)的解決方案可在 GitHub 的 /Chapter_06/Exercises/ 文件夾中找到。若要測(cè)試它們,請(qǐng)將 MyBGList 項(xiàng)目中的相關(guān)文件替換為該文件夾中的文件,然后運(yùn)行應(yīng)用。
將內(nèi)置驗(yàn)證程序添加到 DomainDTO 對(duì)象的 Name 屬性,以便僅當(dāng)它不為 null、不為空且僅包含大寫和小寫字母(不含數(shù)字、空格或任何其他字符)時(shí),它才會(huì)被視為有效。有效值的示例包括“策略”、“系列”和“抽象”。無(wú)效值的示例包括“策略游戲”、“兒童”、“101指南”、“”和 null。
如果值無(wú)效,驗(yàn)證器應(yīng)發(fā)出錯(cuò)誤消息“值必須僅包含字母(不能包含空格、數(shù)字或其他 字符)”。DomainsController 的 Post 方法接受 DomainDTO 復(fù)雜類型作為參數(shù),可用于測(cè)試包含驗(yàn)證結(jié)果的 HTTP 響應(yīng)。
創(chuàng)建一個(gè) [LettersOnly] 驗(yàn)證器屬性,并實(shí)現(xiàn)它以滿足第 6.3.1 節(jié)中給出的相同規(guī)范,包括錯(cuò)誤消息。實(shí)際值檢查應(yīng)使用正則表達(dá)式或字符串操作技術(shù)執(zhí)行,具體取決于自定義 UseRegex 參數(shù)是設(shè)置為 true 還是 false(默認(rèn)值)。當(dāng)自定義驗(yàn)證程序?qū)傩詼?zhǔn)備就緒時(shí),將其應(yīng)用于 MechanicDTO 對(duì)象的 Name 屬性,并使用機(jī)械控制器的 Post 方法使用可用的兩個(gè) UseRegex 參數(shù)值對(duì)其進(jìn)行測(cè)試。
實(shí)現(xiàn) DomainDTO 對(duì)象的 IValidatableObject 接口,并使用其 Valid 方法認(rèn)為僅當(dāng) Id 值等于 3 或 Name 值等于“Wargames”時(shí)才有效。如果模型無(wú)效,驗(yàn)證程序應(yīng)發(fā)出錯(cuò)誤消息“Id 和/或名稱值必須與允許的域匹配”。DomainsController 的 Post 方法接受 DomainDTO 復(fù)雜類型作為參數(shù),可用于測(cè)試包含驗(yàn)證結(jié)果的 HTTP 響應(yīng)。
將 [ManualValidatonFilter] 屬性應(yīng)用于 DomainsController 的 Post 方法,以禁用由 [ApiController] 執(zhí)行的自動(dòng) ModelState 驗(yàn)證。然后實(shí)現(xiàn)手動(dòng)模型狀態(tài)驗(yàn)證,以便在模型狀態(tài)無(wú)效時(shí)有條件地返回以下 HTTP 狀態(tài)代碼:
如果模型狀態(tài)有效,則必須正常處理 HTTP 請(qǐng)求。
修改當(dāng)前 /error 終結(jié)點(diǎn)行為以有條件地返回以下 HTTP 狀態(tài)代碼,具體取決于引發(fā)的異常類型:
若要測(cè)試新的錯(cuò)誤處理實(shí)現(xiàn),請(qǐng)使用最小 API 創(chuàng)建兩個(gè)新的 MapGet 方法并實(shí)現(xiàn)它們,以便它們引發(fā)相應(yīng)類型的異常:
SON解析失敗可能有多種原因,包括JSON格式不正確、JSON數(shù)據(jù)缺失、JSON數(shù)據(jù)類型不匹配、代碼錯(cuò)誤、語(yǔ)法錯(cuò)誤、格式錯(cuò)誤、編碼錯(cuò)誤等。解決方法包括檢查JSON數(shù)據(jù)是否符合JSON格式、檢查JSON數(shù)據(jù)中是否包含特殊字符或非法字符、確認(rèn)JSON數(shù)據(jù)是否完整、確認(rèn)解析JSON數(shù)據(jù)的代碼是否正確、嘗試使用其他JSON解析庫(kù)或工具等。如果使用某個(gè)庫(kù)或框架進(jìn)行JSON解析出現(xiàn)問(wèn)題,可以查看相關(guān)文檔或社區(qū)支持論壇,尋求幫助或解決方案。
JSON(JavaScript Object Notation)是一種輕量級(jí)的數(shù)據(jù)交換格式,通常用于前后端數(shù)據(jù)傳輸。如果您在解析 JSON 數(shù)據(jù)時(shí)遇到了問(wèn)題,可能有以下幾種情況:
1、JSON 格式不正確:JSON 格式要求使用雙引號(hào)表示字符串,屬性名也必須使用雙引號(hào)包括,同時(shí)屬性名和屬性值之間使用冒號(hào)隔開(kāi)。如果這些要求沒(méi)有滿足,解析器可能會(huì)拋出解析錯(cuò)誤。
2、JSON 數(shù)據(jù)缺失:如果 JSON 數(shù)據(jù)中某些屬性缺失,解析器可能無(wú)法正確解析該數(shù)據(jù)。此時(shí)可以通過(guò)檢查數(shù)據(jù)格式,或者在代碼中加入異常處理來(lái)避免出錯(cuò)。
3、JSON 數(shù)據(jù)類型不匹配:JSON 中有多種數(shù)據(jù)類型,包括字符串、數(shù)字、布爾值、數(shù)組和對(duì)象等。如果 JSON 數(shù)據(jù)類型與代碼中期望的不匹配,解析器也可能無(wú)法正確解析數(shù)據(jù)。
4、代碼錯(cuò)誤:有時(shí)候 JSON 解析失敗可能是因?yàn)榇a中存在語(yǔ)法錯(cuò)誤或邏輯錯(cuò)誤。此時(shí)可以檢查代碼并進(jìn)行調(diào)試。
5、語(yǔ)法錯(cuò)誤:JSON數(shù)據(jù)必須遵循特定的語(yǔ)法規(guī)則。如果JSON數(shù)據(jù)中有語(yǔ)法錯(cuò)誤,解析器將無(wú)法正確解析數(shù)據(jù)。請(qǐng)確保JSON數(shù)據(jù)的語(yǔ)法正確,并符合JSON規(guī)范。
6、格式錯(cuò)誤:JSON數(shù)據(jù)必須以JSON對(duì)象或JSON數(shù)組的形式進(jìn)行編寫。如果JSON數(shù)據(jù)不是一個(gè)有效的JSON對(duì)象或數(shù)組,解析器將無(wú)法正確解析數(shù)據(jù)。請(qǐng)檢查JSON數(shù)據(jù)的格式是否正確。
7、編碼錯(cuò)誤:JSON數(shù)據(jù)必須使用正確的字符編碼進(jìn)行編寫。如果JSON數(shù)據(jù)中使用了不支持的字符編碼,解析器將無(wú)法正確解析數(shù)據(jù)。請(qǐng)確保JSON數(shù)據(jù)使用了正確的字符編碼。
8、檢查 JSON 數(shù)據(jù)是否符合 JSON 格式。在 JSON 中,所有屬性名稱必須用雙引號(hào)括起來(lái),字符串也必須用雙引號(hào)括起來(lái),不能使用單引號(hào)。同時(shí),JSON 數(shù)據(jù)必須是有效的 JSON 對(duì)象或 JSON 數(shù)組格式。
9、檢查 JSON 數(shù)據(jù)中是否包含特殊字符或非法字符。例如,如果 JSON 數(shù)據(jù)中包含換行符或回車符等特殊字符,可能會(huì)導(dǎo)致解析失敗。可以嘗試使用 JSON.stringify() 方法將 JSON 數(shù)據(jù)轉(zhuǎn)換為字符串,并使用正則表達(dá)式去除特殊字符。
10、確認(rèn) JSON 數(shù)據(jù)是否完整。如果 JSON 數(shù)據(jù)缺少屬性或值,或者格式不正確,也會(huì)導(dǎo)致解析失敗。可以使用在線 JSON 校驗(yàn)工具檢查 JSON 數(shù)據(jù)是否符合標(biāo)準(zhǔn)格式。
11、確認(rèn)解析 JSON 數(shù)據(jù)的代碼是否正確。可能存在代碼錯(cuò)誤或邏輯錯(cuò)誤,導(dǎo)致解析失敗。可以使用調(diào)試器或日志記錄工具查找代碼問(wèn)題并進(jìn)行修復(fù)。
12、嘗試使用其他 JSON 解析庫(kù)或工具。如果您正在使用的是某個(gè) JSON 解析庫(kù)或工具,可以嘗試使用其他庫(kù)或工具進(jìn)行解析,看是否可以解決問(wèn)題。
13、檢查網(wǎng)絡(luò)連接。如果您的 JSON 數(shù)據(jù)來(lái)源于網(wǎng)絡(luò),可能是網(wǎng)絡(luò)連接問(wèn)題導(dǎo)致解析失敗。可以檢查網(wǎng)絡(luò)連接是否正常,或者嘗試從其他網(wǎng)絡(luò)位置獲取數(shù)據(jù)。
14、檢查數(shù)據(jù)編碼。如果您的 JSON 數(shù)據(jù)使用了非 UTF-8 編碼,可能會(huì)導(dǎo)致解析失敗。可以嘗試將數(shù)據(jù)轉(zhuǎn)換為 UTF-8 編碼,再進(jìn)行解析。
15、檢查解析器設(shè)置。如果您正在使用某個(gè) JSON 解析庫(kù)或工具,可能是解析器設(shè)置有誤導(dǎo)致解析失敗。可以查看相關(guān)文檔或社區(qū)支持論壇,了解正確的解析器設(shè)置方法。
如果是在使用某個(gè)庫(kù)或框架進(jìn)行 JSON 解析時(shí)出現(xiàn)問(wèn)題,可以查看相關(guān)文檔或社區(qū)支持論壇,尋求幫助或解決方案。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。