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 亚洲一区免费,欧美在线视频播放,亚洲一区二区三区在线网站

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          38.JavaScript:try...catch異常處理

          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

          try...catch語(yǔ)句包含兩個(gè)部分:try塊和catch塊。

          • try塊:包圍著可能會(huì)拋出錯(cuò)誤的代碼。
          • catch塊:當(dāng)try塊中的代碼拋出錯(cuò)誤時(shí)執(zhí)行的代碼塊。

          如果try塊中的代碼運(yùn)行正常,則跳過(guò)catch塊。如果try塊中的代碼拋出錯(cuò)誤,則立即停止執(zhí)行try塊中的剩余代碼,并跳轉(zhuǎn)到catch塊。

          基本語(yǔ)法

          try {
              // 嘗試執(zhí)行的代碼
          } catch (error) {
              // 發(fā)生錯(cuò)誤時(shí)執(zhí)行的代碼
          }
          

          示例1:捕獲語(yǔ)法錯(cuò)誤

          <!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ò)誤信息。

          示例2:處理JSON解析錯(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ò)誤信息。

          示例3:處理DOM操作錯(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ò)誤信息。

          示例4:使用 finally 語(yǔ)句

          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í)行。

          總結(jié)

          try...catch是處理JavaScript中錯(cuò)誤的有效方式,它可以幫助我們捕獲運(yùn)行時(shí)錯(cuò)誤,并根據(jù)需要進(jìn)行處理。通過(guò)合理使用try...catch,我們的應(yīng)用程序可以更加健壯和可靠。記住,錯(cuò)誤處理不僅僅是捕獲錯(cuò)誤,更重要的是如何根據(jù)不同的錯(cuò)誤類型給用戶提供有用的反饋和恢復(fù)程序的運(yùn)行。

          章涵蓋

          • 模型綁定和數(shù)據(jù)驗(yàn)證概述
          • 內(nèi)置和自定義驗(yàn)證屬性
          • 模型狀態(tài)驗(yàn)證方法
          • 錯(cuò)誤和異常處理技術(shù)

          為簡(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è)主要概念:

          • 數(shù)據(jù)驗(yàn)證 - 一組方法、檢查、例程和規(guī)則,用于確保進(jìn)入我們系統(tǒng)的數(shù)據(jù)有意義、準(zhǔn)確和安全,因此允許進(jìn)行處理
          • 錯(cuò)誤處理 — 預(yù)測(cè)、檢測(cè)、分類和管理程序執(zhí)行流中可能發(fā)生的應(yīng)用程序錯(cuò)誤的過(guò)程

          在接下來(lái)的部分中,我們將了解如何在代碼中將它們付諸實(shí)踐。

          6.1 數(shù)據(jù)驗(yàn)證

          我們從第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):

          • pageIndex - 一個(gè)可選的整數(shù)值,用于設(shè)置要返回的棋盤游戲的起始頁(yè)
          • pageSize - 用于設(shè)置每個(gè)頁(yè)面大小的可選整數(shù)值
          • sortColumn - 一個(gè)可選的字符串值,用于設(shè)置列以對(duì)返回的棋盤游戲進(jìn)行排序
          • 排序順序 - 用于設(shè)置排序順序的可選字符串值
          • filterQuery - 一個(gè)可選的字符串值,如果存在,將僅用于返回名稱包含它的棋盤游戲

          所有這些參數(shù)都是可選的。我們選擇允許它們不存在(換句話說(shuō),在沒(méi)有它們的情況下接受傳入請(qǐng)求),因?yàn)槲覀兛梢暂p松提供合適的默認(rèn)值,以防調(diào)用方未顯式提供它們。因此,以下所有 HTTP 請(qǐng)求都將以相同的方式處理,因此將提供相同的結(jié)果(直到默認(rèn)值更改):

          • https://localhost:40443/BoardGames
          • https://localhost:40443/BoardGames?pageIndex=0&pageSize=10
          • https://localhost:40443/BoardGames?pageIndex=0&pageSize=10&sortColumn=Name&sortOrder=ASC

          同時(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):

          • 為每個(gè)參數(shù)提供一個(gè)未定義的值是可以的,因?yàn)槲覀冇蟹?wù)器定義的回退(操作方法的默認(rèn)值)。
          • 為 pageIndex 和 pageSize 提供非整數(shù)值是不行的,因?yàn)槲覀兿M鼈兪钦麛?shù)類型。

          我們顯然是在談?wù)撾[式活動(dòng),因?yàn)榭諜z查和回退到默認(rèn)值的任務(wù)是由底層框架執(zhí)行的,而無(wú)需我們編寫任何內(nèi)容。具體來(lái)說(shuō),我們正在利用 ASP.NET Core的模型綁定系統(tǒng),這是自動(dòng)處理所有這些的機(jī)制。

          6.1.1 模型綁定

          來(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ù):

          • 標(biāo)識(shí)頁(yè)面索引和頁(yè)面大小 GET 參數(shù)的存在
          • 檢索其原始字符串值(“2”和“50”),將其轉(zhuǎn)換為整數(shù)類型(2 和 50),并將轉(zhuǎn)換后的值分配給相應(yīng)操作方法的屬性
          • 標(biāo)識(shí)缺少 sortColumn、sortOrder 和 filterQuery GET 參數(shù),并將 null 值分配給相應(yīng)操作方法的屬性,以便改用相應(yīng)的默認(rèn)值

          簡(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]),我們將在后面看到。

          6.1.2 數(shù)據(jù)驗(yàn)證屬性

          除了執(zhí)行標(biāo)準(zhǔn)類型轉(zhuǎn)換之外,還可以將模型綁定配置為使用 System.ComponentModel.DataAnnotation 命名空間中包含的一組內(nèi)置數(shù)據(jù)批注屬性來(lái)執(zhí)行多個(gè)數(shù)據(jù)驗(yàn)證任務(wù)。以下是這些屬性中最值得注意的列表:

          • [信用卡] - 確保給定輸入是信用卡號(hào)
          • [電子郵件地址] - 確保給定的字符串輸入具有電子郵件地址格式
          • [MaxLength(n)] - 確保給定字符串或數(shù)組輸入的長(zhǎng)度小于或等于指定值
          • [最小長(zhǎng)度 (n)] - 確保給定字符串或數(shù)組輸入的長(zhǎng)度等于或大于指定值
          • [范圍(nMin, nMax)] - 確保給定輸入介于指定值的最小值和最大值之間
          • [正則表達(dá)式(正則表達(dá)式)] - 確保給定的輸入與給定的正則表達(dá)式匹配
          • [必需] - 確保給定輸入具有非空值
          • [字符串長(zhǎng)度] - 確保給定的字符串輸入不超過(guò)指定的長(zhǎng)度限制
          • [Url] - 確保給定的字符串輸入具有 URL 格式

          學(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ì)使代碼混亂。

          6.1.3 一個(gè)非平凡的驗(yàn)證示例

          我們用來(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á)式(也稱為 RegExRegExp)是用于匹配字符串中的字符組合的標(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í)行以下步驟:

          1. 添加新的類文件,該文件將包含自定義驗(yàn)證器的源代碼。
          2. 擴(kuò)展驗(yàn)證屬性基類。
          3. 用我們自己的實(shí)現(xiàn)重寫 IsValid 方法。
          4. 配置并返回包含結(jié)果的驗(yàn)證結(jié)果對(duì)象。

          添加 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í)體表示。我們可以采取以下兩種方法之一:

          • 使用固定字符串對(duì)所有 BoardGame 實(shí)體的屬性名稱進(jìn)行硬編碼,并像處理“ASC”和“DESC”值一樣繼續(xù)。
          • 找到一種方法來(lái)根據(jù)給定的輸入字符串動(dòng)態(tài)檢查實(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è)原因,我建議始終遵循這種做法,即使它需要額外的工作。

          6.1.4 數(shù)據(jù)驗(yàn)證和開(kāi)放API

          由于 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í)行以下操作:

          1. 創(chuàng)建一個(gè)新的篩選器類,為每個(gè)自定義驗(yàn)證屬性實(shí)現(xiàn) IParameterFilter 接口。Swashbuckle 將在為控制器的操作方法(和最小 API 方法)使用的所有參數(shù)創(chuàng)建 JSON 塊之前調(diào)用并執(zhí)行此過(guò)濾器。
          2. 實(shí)現(xiàn) IParameterFilter 接口的 Apply 方法,以便它檢測(cè)使用我們的自定義驗(yàn)證屬性修飾的所有參數(shù),并將每個(gè)參數(shù)的相關(guān)信息添加到 swagger.json 文件中。

          讓我們把這個(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)建和記錄它們可以有所作為。

          6.1.5 綁定復(fù)雜類型

          到目前為止,我們一直使用簡(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] 屬性告訴路由中間件我們希望從查詢字符串中獲取輸入值,從而保留以前的行為。但是我們可以使用任何其他可用屬性:

          • [FromQuery] - 從查詢字符串中獲取值
          • [FromRoute] - 從路徑數(shù)據(jù)中獲取值
          • [發(fā)件人表單] - 從已發(fā)布的表單域中獲取值
          • [FromBody] - 從請(qǐng)求正文獲取值
          • [FromHeader] - 從 HTTP 標(biāo)頭獲取值
          • [FromServices] - 從已注冊(cè)服務(wù)的實(shí)例中獲取值
          • [FromUri] - 從外部 URI 獲取值

          能夠使用基于屬性的方法在參數(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):

          • 它不需要在編譯時(shí)定義,因此它可以使用泛型類型(這使我們能夠克服我們的問(wèn)題)。
          • 它旨在驗(yàn)證整個(gè)類,因此我們可以使用它來(lái)同時(shí)檢查多個(gè)屬性,并執(zhí)行交叉驗(yàn)證和任何其他需要整體方法的任務(wù)。

          讓我們使用 IValidatableObject 接口在當(dāng)前的 RequestDTO 類中實(shí)現(xiàn)排序列驗(yàn)證檢查。以下是我們需要做的:

          1. 更改 RequestDTO 的類聲明,以便它可以接受泛型 <T> 類型。
          2. 將 IValidatableObject 接口添加到 RequestDTO 類型。
          3. 實(shí)現(xiàn) IValidatableObject 接口的 Validate 方法,以便它將提取泛型 <T> 類型的屬性,并使用其名稱來(lái)驗(yàn)證 SortColumn 屬性。

          下面的清單顯示了我們?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 方法)徹底檢查它們,

          • https://localhost:40443/Domains/
          • https://localhost:40443/Mechanics/

          以及 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ò)誤和程序異常。

          6.2 錯(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)置功能,用于裝飾我們的控制器。

          6.2.1 模型狀態(tài)對(duì)象

          若要了解 [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ù):

          • 將輸入值綁定到操作方法的簡(jiǎn)單類型和/或復(fù)雜類型參數(shù)。如果綁定過(guò)程失敗,則會(huì)立即返回 HTTP 錯(cuò)誤 400 響應(yīng);否則,請(qǐng)求將進(jìn)入下一階段。
          • 使用內(nèi)置驗(yàn)證屬性、自定義驗(yàn)證屬性和/或 IValidatableObject 驗(yàn)證模型。所有驗(yàn)證檢查的結(jié)果都記錄在 ModelState 對(duì)象中,該對(duì)象最終變?yōu)橛行Вㄎ窗l(fā)生驗(yàn)證錯(cuò)誤)或無(wú)效(發(fā)生一個(gè)或多個(gè)驗(yàn)證錯(cuò)誤)。如果 ModelState 對(duì)象最終有效,則請(qǐng)求由操作方法處理;否則,將返回 HTTP 錯(cuò)誤 400 響應(yīng)。

          重要的教訓(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í)行此操作。

          6.2.2 自定義錯(cuò)誤消息

          我們可能要做的最重要的事情是定義一些自定義錯(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í)行。

          6.2.3 手動(dòng)模型驗(yàn)證

          假設(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 操作方法。以下是我們需要做的:

          1. 檢查模型狀態(tài)(有效或無(wú)效)。
          2. 如果模型狀態(tài)有效,請(qǐng)保留現(xiàn)有行為。
          3. 如果模型狀態(tài)無(wú)效,請(qǐng)檢查錯(cuò)誤是否與 pageSize 參數(shù)相關(guān)。如果是這種情況,請(qǐng)返回 HTTP 501 狀態(tài)代碼;否則,請(qǐng)堅(jiān)持使用 HTTP 400。

          以下是我們?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:

          • https://localhost:40443/BoardGames?pageSize=101
          • https://localhost:40443/BoardGames?sortOrder=invalidValue

          第一個(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é)束,至少目前是這樣。

          6.2.4 異常處理

          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ì)象:

          • 當(dāng)前 HttpContext,可以作為參數(shù)添加到所有最小 API 方法中
          • 一個(gè) IExceptionHandlerPathFeature 接口實(shí)例,它允許我們?cè)诜奖愕奶幚沓绦蛑性L問(wèn)原始異常

          以下是我們?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í)行以下操作:

          • 根據(jù)異常的類型返回不同的 HTTP 狀態(tài)代碼,就像我們?cè)?DomainsController 的 Get 方法中使用 ModelState 手動(dòng)驗(yàn)證所做的那樣。
          • 在某處記錄異常,例如在我們的 DBMS 中
          • 向管理員、審核員和/或其他方發(fā)送電子郵件通知

          其中一些可能性將在后面的章節(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)用程序日志記錄的主題。

          6.3 練習(xí)

          是時(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)用。

          6.3.1 內(nèi)置驗(yàn)證器

          將內(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)。

          6.3.2 自定義驗(yàn)證器

          創(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è)試。

          6.3.3 可識(shí)別對(duì)象

          實(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)。

          6.3.4 模型狀態(tài)驗(yàn)證

          將 [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)代碼:

          • HTTP 403 - 禁止訪問(wèn) - 如果 ModelState 無(wú)效,因?yàn)?Id 值不等于 3 且名稱值不等于“Wargames”
          • HTTP 400 - 錯(cuò)誤請(qǐng)求 - 如果模型狀態(tài)因任何其他原因無(wú)效

          如果模型狀態(tài)有效,則必須正常處理 HTTP 請(qǐng)求。

          6.3.5 異常處理

          修改當(dāng)前 /error 終結(jié)點(diǎn)行為以有條件地返回以下 HTTP 狀態(tài)代碼,具體取決于引發(fā)的異常類型:

          • HTTP 501 - 未實(shí)現(xiàn) - 對(duì)于未實(shí)現(xiàn)的異常類型
          • HTTP 504 - 網(wǎng)關(guān)超時(shí) - 對(duì)于超時(shí)異常類型
          • HTTP 500 - 內(nèi)部服務(wù)器錯(cuò)誤 - 對(duì)于任何其他異常類型

          若要測(cè)試新的錯(cuò)誤處理實(shí)現(xiàn),請(qǐng)使用最小 API 創(chuàng)建兩個(gè)新的 MapGet 方法并實(shí)現(xiàn)它們,以便它們引發(fā)相應(yīng)類型的異常:

          • /error/test/501 的 HTTP 501 - 未實(shí)現(xiàn)狀態(tài)代碼
          • /error/test/504 用于 HTTP 504 - 網(wǎng)關(guān)超時(shí)狀態(tài)代碼

          總結(jié)

          • 數(shù)據(jù)驗(yàn)證和錯(cuò)誤處理使我們能夠在客戶端和服務(wù)器之間的交互過(guò)程中處理大多數(shù)意外情況,從而降低數(shù)據(jù)泄漏、速度變慢以及其他安全和性能問(wèn)題的風(fēng)險(xiǎn)。
          • ASP.NET Core 模型綁定系統(tǒng)負(fù)責(zé)處理來(lái)自 HTTP 請(qǐng)求的所有輸入數(shù)據(jù),包括將它們轉(zhuǎn)換為 .NET 類型(綁定)并根據(jù)我們的數(shù)據(jù)驗(yàn)證規(guī)則檢查它們(驗(yàn)證)。
          • 我們可以通過(guò)使用內(nèi)置或自定義數(shù)據(jù)注釋屬性將數(shù)據(jù)驗(yàn)證規(guī)則分配給輸入?yún)?shù)和復(fù)雜類型屬性。此外,我們可以使用 IValidatableObject 接口在復(fù)雜類型中創(chuàng)建交叉驗(yàn)證檢查。
          • ModelState 對(duì)象包含針對(duì)輸入?yún)?shù)執(zhí)行的數(shù)據(jù)驗(yàn)證檢查的組合結(jié)果。ASP.NET Core 允許我們以兩種方式使用它:
            • 自動(dòng)處理它(感謝 [ApiController] 的自動(dòng)驗(yàn)證功能)。
            • 手動(dòng)檢查其值,這使我們能夠自定義整個(gè)驗(yàn)證過(guò)程和生成的 HTTP 響應(yīng)。
          • 應(yīng)用程序級(jí)錯(cuò)誤和異常可以使用 ExceptionHandling 中間件進(jìn)行處理。此中間件可以配置為根據(jù)我們的需求自定義錯(cuò)誤處理體驗(yàn),例如
            • 根據(jù)異常的類型返回不同的 HTTP 狀態(tài)代碼和/或人類可讀的信息。
            • 在某處記錄異常(DBMS、文本文件、事件注冊(cè)表等)。
            • 向相關(guān)方(如系統(tǒng)管理員)發(fā)送電子郵件通知。

          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ū)支持論壇,尋求幫助或解決方案。


          主站蜘蛛池模板: 精品久久国产一区二区三区香蕉| 性无码一区二区三区在线观看| 中文字幕一区二区人妻| 久久婷婷久久一区二区三区| 国产日韩综合一区二区性色AV| 99久久国产精品免费一区二区| 亚洲av午夜精品一区二区三区| 无码日韩AV一区二区三区| 无码国产伦一区二区三区视频| 久久综合精品国产一区二区三区| 日韩视频一区二区| 国产亚洲情侣一区二区无| 女人和拘做受全程看视频日本综合a一区二区视频 | 久久一区二区明星换脸| 国产在线精品一区二区| 国产精品无码一区二区在线 | 日韩在线视频一区| 少妇激情av一区二区| 熟女大屁股白浆一区二区| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 无码一区二区三区视频| 久久精品一区二区影院| 上原亚衣一区二区在线观看| 夜夜爽一区二区三区精品| 日本一区二区三区精品国产| 国产成人一区二区三区免费视频| 国产主播在线一区| 伊人色综合视频一区二区三区| 亚洲欧洲日韩国产一区二区三区 | 成人区人妻精品一区二区三区| 无码中文字幕一区二区三区| 久久精品无码一区二区三区不卡 | 一区二区三区亚洲| 97一区二区三区四区久久| 无码人妻一区二区三区免费| 亚洲丰满熟女一区二区哦| 日韩精品一区二区三区中文版 | 无码人妻精品一区二区蜜桃百度 | 色精品一区二区三区| 福利一区福利二区| 久久精品一区二区国产|